Merge remote-tracking branch 'template/main'

This commit is contained in:
thepaperpilot 2022-03-11 19:17:28 -06:00
commit a2e490c291
39 changed files with 342 additions and 212 deletions

View file

@ -6,6 +6,43 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
## [0.1.3] - 2022-03-11
### Added
- Milestone.complete
- Challenge.complete
- setupAutoClick function to run a clickable's onClick every tick
- setupAutoComplete function to attempt to complete a challenge every tick
- isAnyChallengeActive function to query if any challenge from a given list is active
- Hotkeys now appear in info modal, if any exist
- projInfo.json now includes a "enablePausing" option that can be used to prevent the player from pausing the game
- Added a "gameWon" global event
### Changed
- **BREAKING** Buyables now default to an infinite purchase limit
- **BREAKING** devSpeed, playedTime, offlineTime, and diff now use numbers instead of Decimals
- **BREAKING** Achievements and milestones now use watchEffect to check for completion, instead of polling each tick. shouldEarn properties now only accept functions
- Cached more decimal values for optimization
### Fixed
- Many types not being exported
- setupHoldToClick wouldn't stop clicking after a component is unmounted
- Header's banner would not have correct width
### Removed
- **BREAKING** Removed setupAutoReset
### Documentation
- Support for documentation generation using typedoc
- Hide main layer from docs
- Hide prestige layer from docs
- Use stub declaration files for libs that don't provide types (vue-panzoom and vue-textarea-autosize)
## [0.1.2] - 2022-03-05
### Changed
- **BREAKING** Removed "@" path alias, and used baseUrl instead
- **BREAKING** Renamed createExponentialScaling to createPolynomialScaling and removed coefficient parameter
- Changed options passed into createLayerTreeNode; now allows overriding display
- App component is no longer cloned before being passed to `createApp`
- Changed TS version from ^4.5.4 to ~4.5.5
### Fixed
- Document title is set as soon as possible now
## [0.1.1] - 2022-03-02
### Added
- Configuration for Glitch projects

View file

@ -1,6 +1,6 @@
{
"name": "profectus",
"version": "0.1.1",
"version": "0.1.3",
"private": true,
"scripts": {
"start": "vue-cli-service serve",

View file

@ -44,17 +44,21 @@
</div>
<br />
<div>Time Played: {{ timePlayed }}</div>
<component :is="infoComponent" />
</div>
</template>
</Modal>
</template>
<script setup lang="ts">
<script setup lang="tsx">
import Modal from "components/Modal.vue";
import type Changelog from "data/Changelog.vue";
import projInfo from "data/projInfo.json";
import { jsx } from "features/feature";
import player from "game/player";
import { infoComponents } from "game/settings";
import { formatTime } from "util/bignum";
import { coerceComponent, render } from "util/vue";
import { computed, ref, toRefs, unref } from "vue";
const { title, logo, author, discordName, discordLink, versionNumber, versionTitle } = projInfo;
@ -66,6 +70,10 @@ const isOpen = ref(false);
const timePlayed = computed(() => formatTime(player.timePlayed));
const infoComponent = computed(() => {
return coerceComponent(jsx(() => <>{infoComponents.map(render)}</>));
});
defineExpose({
open() {
isOpen.value = true;

View file

@ -21,7 +21,7 @@
</div>
<br />
<Toggle title="Autosave" v-model="autosave" />
<Toggle title="Pause game" v-model="isPaused" />
<Toggle v-if="projInfo.enablePausing" title="Pause game" v-model="isPaused" />
</template>
<template v-slot:footer>
<div class="nan-footer">

View file

@ -1,6 +1,6 @@
<template>
<div class="nav" v-if="useHeader" v-bind="$attrs">
<img v-if="banner" :src="banner" height="100%" :alt="title" />
<img v-if="banner" :src="banner" class="banner" :alt="title" />
<div v-else class="title">{{ title }}</div>
<div @click="changelog?.open()" class="version-container">
<Tooltip display="Changelog" bottom class="version"
@ -141,6 +141,11 @@ function openDiscord() {
flex-shrink: 0;
}
.nav > .banner {
height: 100%;
width: unset;
}
.overlay-nav {
position: absolute;
top: 10px;

View file

@ -13,23 +13,24 @@
<Toggle title="Unthrottled" v-model="unthrottled" />
<Toggle :title="offlineProdTitle" v-model="offlineProd" />
<Toggle :title="autosaveTitle" v-model="autosave" />
<Toggle :title="isPausedTitle" v-model="isPaused" />
<Toggle v-if="projInfo.enablePausing" :title="isPausedTitle" v-model="isPaused" />
</template>
</Modal>
</template>
<script setup lang="tsx">
import Modal from "components/Modal.vue";
import projInfo from "data/projInfo.json";
import rawThemes from "data/themes";
import { jsx } from "features/feature";
import player from "game/player";
import settings, { settingFields } from "game/settings";
import { camelToTitle } from "util/common";
import { computed, ref, toRefs } from "vue";
import Toggle from "./fields/Toggle.vue";
import Select from "./fields/Select.vue";
import Tooltip from "./Tooltip.vue";
import { jsx } from "features/feature";
import { coerceComponent, render } from "util/vue";
import { computed, ref, toRefs } from "vue";
import Select from "./fields/Select.vue";
import Toggle from "./fields/Toggle.vue";
import Tooltip from "./Tooltip.vue";
const isOpen = ref(false);

View file

@ -35,7 +35,7 @@ export interface ResetButtonOptions extends ClickableOptions {
canClick?: Computable<boolean>;
}
type ResetButton<T extends ResetButtonOptions> = Replace<
export type ResetButton<T extends ResetButtonOptions> = Replace<
Clickable<T>,
{
resetDescription: GetComputableTypeWithDefault<T["resetDescription"], Ref<string>>;

View file

@ -1,3 +1,7 @@
/**
* @module
* @hidden
*/
import { main } from "data/projEntry";
import { createCumulativeConversion, createPolynomialScaling } from "features/conversion";
import { jsx } from "features/feature";
@ -5,7 +9,7 @@ import { createReset } from "features/reset";
import MainDisplay from "features/resources/MainDisplay.vue";
import { createResource } from "features/resources/resource";
import { createLayer } from "game/layers";
import { DecimalSource } from "lib/break_eternity";
import { DecimalSource } from "util/bignum";
import { render } from "util/vue";
import { createLayerTreeNode, createResetButton } from "../common";

View file

@ -6,14 +6,16 @@ import { branchedResetPropagation, createTree, GenericTree } from "features/tree
import { globalBus } from "game/events";
import { createLayer, GenericLayer, setupLayerModal } from "game/layers";
import player, { PlayerData } from "game/player";
import { DecimalSource } from "lib/break_eternity";
import Decimal, { format, formatTime } from "util/bignum";
import Decimal, { DecimalSource, format, formatTime } from "util/bignum";
import { render } from "util/vue";
import { computed, toRaw } from "vue";
import a from "./layers/aca/a";
import c from "./layers/aca/c";
import f from "./layers/aca/f";
/**
* @hidden
*/
export const main = createLayer(() => {
const points = createResource<DecimalSource>(10);
const best = trackBest(points);

View file

@ -17,5 +17,6 @@
"initialTabs": [ "main", "c" ],
"maxTickLength": 3600,
"offlineLimit": 1
"offlineLimit": 1,
"enablePausing": true
}

View file

@ -1,4 +1,4 @@
interface ThemeVars {
export interface ThemeVars {
"--foreground": string;
"--background": string;
"--feature-foreground": string;

View file

@ -2,7 +2,6 @@ import AchievementComponent from "features/achievements/Achievement.vue";
import {
CoercableComponent,
Component,
findFeatures,
GatherProps,
getUniqueID,
Replace,
@ -10,7 +9,6 @@ import {
StyleValue,
Visibility
} from "features/feature";
import { globalBus } from "game/events";
import "game/notifications";
import { Persistent, makePersistent, PersistentState } from "game/persistence";
import {
@ -22,15 +20,16 @@ import {
} from "util/computed";
import { createLazyProxy } from "util/proxies";
import { coerceComponent } from "util/vue";
import { Unsubscribe } from "nanoevents";
import { Ref, unref } from "vue";
import { Ref, unref, watchEffect } from "vue";
import { useToast } from "vue-toastification";
const toast = useToast();
export const AchievementType = Symbol("Achievement");
export interface AchievementOptions {
visibility?: Computable<Visibility>;
shouldEarn?: Computable<boolean>;
shouldEarn?: () => boolean;
display?: Computable<CoercableComponent>;
mark?: Computable<boolean | string>;
image?: Computable<string>;
@ -39,7 +38,7 @@ export interface AchievementOptions {
onComplete?: VoidFunction;
}
interface BaseAchievement extends Persistent<boolean> {
export interface BaseAchievement extends Persistent<boolean> {
id: string;
earned: Ref<boolean>;
complete: VoidFunction;
@ -52,7 +51,6 @@ export type Achievement<T extends AchievementOptions> = Replace<
T & BaseAchievement,
{
visibility: GetComputableTypeWithDefault<T["visibility"], Visibility.Visible>;
shouldEarn: GetComputableType<T["shouldEarn"]>;
display: GetComputableType<T["display"]>;
mark: GetComputableType<T["mark"]>;
image: GetComputableType<T["image"]>;
@ -85,7 +83,6 @@ export function createAchievement<T extends AchievementOptions>(
processComputable(achievement as T, "visibility");
setDefault(achievement, "visibility", Visibility.Visible);
processComputable(achievement as T, "shouldEarn");
processComputable(achievement as T, "display");
processComputable(achievement as T, "mark");
processComputable(achievement as T, "image");
@ -97,29 +94,18 @@ export function createAchievement<T extends AchievementOptions>(
return { visibility, display, earned, image, style, classes, mark, id };
};
return achievement as unknown as Achievement<T>;
});
}
const toast = useToast();
const listeners: Record<string, Unsubscribe | undefined> = {};
globalBus.on("addLayer", layer => {
const achievements: GenericAchievement[] = (
findFeatures(layer, AchievementType) as GenericAchievement[]
).filter(ach => ach.shouldEarn != null);
if (achievements.length) {
listeners[layer.id] = layer.on("postUpdate", () => {
achievements.forEach(achievement => {
if (achievement.shouldEarn) {
const genericAchievement = achievement as GenericAchievement;
watchEffect(() => {
if (
unref(achievement.visibility) === Visibility.Visible &&
!unref(achievement.earned) &&
unref(achievement.shouldEarn)
!genericAchievement.earned.value &&
unref(genericAchievement.visibility) === Visibility.Visible &&
genericAchievement.shouldEarn?.()
) {
achievement[PersistentState].value = true;
achievement.onComplete?.();
if (achievement.display) {
const Display = coerceComponent(unref(achievement.display));
genericAchievement.earned.value = true;
genericAchievement.onComplete?.();
if (genericAchievement.display) {
const Display = coerceComponent(unref(genericAchievement.display));
toast.info(
<div>
<h3>Achievement earned!</h3>
@ -133,11 +119,8 @@ globalBus.on("addLayer", layer => {
}
}
});
});
}
});
globalBus.on("removeLayer", layer => {
// unsubscribe from postUpdate
listeners[layer.id]?.();
listeners[layer.id] = undefined;
});
}
return achievement as unknown as Achievement<T>;
});
}

View file

@ -9,7 +9,7 @@ import {
StyleValue,
Visibility
} from "features/feature";
import { DecimalSource } from "lib/break_eternity";
import { DecimalSource } from "util/bignum";
import {
Computable,
GetComputableType,
@ -45,7 +45,7 @@ export interface BarOptions {
mark?: Computable<boolean | string>;
}
interface BaseBar {
export interface BaseBar {
id: string;
type: typeof BarType;
[Component]: typeof BarComponent;

View file

@ -11,7 +11,6 @@ import {
} from "features/feature";
import { globalBus } from "game/events";
import { State, Persistent, makePersistent, PersistentState } from "game/persistence";
import Decimal, { DecimalSource } from "lib/break_eternity";
import { isFunction } from "util/common";
import {
Computable,
@ -85,7 +84,7 @@ export interface NodeTypeOptions {
actionDistance?: NodeComputable<number>;
onClick?: (node: BoardNode) => void;
onDrop?: (node: BoardNode, otherNode: BoardNode) => void;
update?: (node: BoardNode, diff: DecimalSource) => void;
update?: (node: BoardNode, diff: number) => void;
}
export interface BaseNodeType {
@ -167,7 +166,7 @@ export interface BoardOptions {
types: Record<string, NodeTypeOptions>;
}
interface BaseBoard extends Persistent<BoardData> {
export interface BaseBoard extends Persistent<BoardData> {
id: string;
links: Ref<BoardNodeLink[] | null>;
nodes: Ref<BoardNode[]>;
@ -348,7 +347,7 @@ export function getUniqueNodeID(board: GenericBoard): number {
const listeners: Record<string, Unsubscribe | undefined> = {};
globalBus.on("addLayer", layer => {
const boards: GenericBoard[] = findFeatures(layer, BoardType) as GenericBoard[];
listeners[layer.id] = layer.on("postUpdate", (diff: Decimal) => {
listeners[layer.id] = layer.on("postUpdate", diff => {
boards.forEach(board => {
Object.values(board.types).forEach(type =>
type.nodes.value.forEach(node => type.update?.(node, diff))

View file

@ -26,7 +26,7 @@ import {
export const BuyableType = Symbol("Buyable");
type BuyableDisplay =
export type BuyableDisplay =
| CoercableComponent
| {
title?: CoercableComponent;
@ -48,7 +48,7 @@ export interface BuyableOptions {
onPurchase?: (cost: DecimalSource) => void;
}
interface BaseBuyable extends Persistent<DecimalSource> {
export interface BaseBuyable extends Persistent<DecimalSource> {
id: string;
amount: Ref<DecimalSource>;
maxed: Ref<boolean>;
@ -68,7 +68,7 @@ export type Buyable<T extends BuyableOptions> = Replace<
cost: GetComputableType<T["cost"]>;
resource: GetComputableType<T["resource"]>;
canPurchase: GetComputableTypeWithDefault<T["canPurchase"], Ref<boolean>>;
purchaseLimit: GetComputableTypeWithDefault<T["purchaseLimit"], 1>;
purchaseLimit: GetComputableTypeWithDefault<T["purchaseLimit"], Decimal>;
classes: GetComputableType<T["classes"]>;
style: GetComputableType<T["style"]>;
mark: GetComputableType<T["mark"]>;
@ -172,6 +172,16 @@ export function createBuyable<T extends BuyableOptions>(
const Title = coerceComponent(currDisplay.title || "", "h3");
const Description = coerceComponent(currDisplay.description);
const EffectDisplay = coerceComponent(currDisplay.effectDisplay || "");
const amountDisplay =
unref(genericBuyable.purchaseLimit) === Decimal.dInf ? (
<>Amount: {formatWhole(genericBuyable.amount.value)}</>
) : (
<>
Amount: {formatWhole(genericBuyable.amount.value)} /{" "}
{formatWhole(unref(genericBuyable.purchaseLimit))}
</>
);
return (
<span>
{currDisplay.title ? (
@ -182,8 +192,7 @@ export function createBuyable<T extends BuyableOptions>(
<Description />
<div>
<br />
Amount: {formatWhole(genericBuyable.amount.value)} /{" "}
{formatWhole(unref(genericBuyable.purchaseLimit))}
{amountDisplay}
</div>
{currDisplay.effectDisplay ? (
<div>
@ -209,7 +218,7 @@ export function createBuyable<T extends BuyableOptions>(
processComputable(buyable as T, "cost");
processComputable(buyable as T, "resource");
processComputable(buyable as T, "purchaseLimit");
setDefault(buyable, "purchaseLimit", 1);
setDefault(buyable, "purchaseLimit", Decimal.dInf);
processComputable(buyable as T, "style");
processComputable(buyable as T, "mark");
processComputable(buyable as T, "small");

View file

@ -1,3 +1,4 @@
import { isArray } from "@vue/shared";
import Toggle from "components/fields/Toggle.vue";
import ChallengeComponent from "features/challenges/Challenge.vue";
import {
@ -25,7 +26,7 @@ import {
ProcessedComputable
} from "util/computed";
import { createLazyProxy } from "util/proxies";
import { computed, Ref, unref } from "vue";
import { computed, Ref, unref, watch, WatchStopHandle } from "vue";
export const ChallengeType = Symbol("ChallengeType");
@ -55,13 +56,14 @@ export interface ChallengeOptions {
onEnter?: VoidFunction;
}
interface BaseChallenge {
export interface BaseChallenge {
id: string;
completions: PersistentRef<DecimalSource>;
completed: Ref<boolean>;
maxed: Ref<boolean>;
active: PersistentRef<boolean>;
toggle: VoidFunction;
complete: (remainInChallenge?: boolean) => void;
type: typeof ChallengeType;
[Component]: typeof ChallengeComponent;
[GatherProps]: () => Record<string, unknown>;
@ -87,18 +89,12 @@ export type GenericChallenge = Replace<
{
visibility: ProcessedComputable<Visibility>;
canStart: ProcessedComputable<boolean>;
canComplete: ProcessedComputable<boolean>;
canComplete: ProcessedComputable<boolean | DecimalSource>;
completionLimit: ProcessedComputable<DecimalSource>;
mark: ProcessedComputable<boolean>;
}
>;
export function createActiveChallenge(
challenges: GenericChallenge[]
): Ref<GenericChallenge | undefined> {
return computed(() => challenges.find(challenge => challenge.active.value));
}
export function createChallenge<T extends ChallengeOptions>(
optionsFunc: () => T & ThisType<Challenge<T>>
): Challenge<T> {
@ -134,11 +130,7 @@ export function createChallenge<T extends ChallengeOptions>(
challenge.toggle = function () {
const genericChallenge = challenge as GenericChallenge;
if (genericChallenge.active.value) {
if (
genericChallenge.canComplete &&
unref(genericChallenge.canComplete) &&
!genericChallenge.maxed.value
) {
if (unref(genericChallenge.canComplete) && !genericChallenge.maxed.value) {
let completions: boolean | DecimalSource = unref(genericChallenge.canComplete);
if (typeof completions === "boolean") {
completions = 1;
@ -152,12 +144,40 @@ export function createChallenge<T extends ChallengeOptions>(
genericChallenge.active.value = false;
genericChallenge.onExit?.();
genericChallenge.reset?.reset();
} else if (unref(genericChallenge.canStart)) {
} else if (
unref(genericChallenge.canStart) &&
unref(genericChallenge.visibility) === Visibility.Visible &&
!genericChallenge.maxed.value
) {
genericChallenge.reset?.reset();
genericChallenge.active.value = true;
genericChallenge.onEnter?.();
}
};
challenge.complete = function (remainInChallenge?: boolean) {
const genericChallenge = challenge as GenericChallenge;
let completions: boolean | DecimalSource = unref(genericChallenge.canComplete);
if (
genericChallenge.active.value &&
completions !== false &&
(completions === true || Decimal.neq(0, completions)) &&
!genericChallenge.maxed.value
) {
if (typeof completions === "boolean") {
completions = 1;
}
genericChallenge.completions.value = Decimal.min(
Decimal.add(genericChallenge.completions.value, completions),
unref(genericChallenge.completionLimit)
);
genericChallenge.onComplete?.();
if (remainInChallenge !== true) {
genericChallenge.active.value = false;
genericChallenge.onExit?.();
genericChallenge.reset?.reset();
}
}
};
processComputable(challenge as T, "visibility");
setDefault(challenge, "visibility", Visibility.Visible);
const visibility = challenge.visibility as ProcessedComputable<Visibility>;
@ -167,16 +187,6 @@ export function createChallenge<T extends ChallengeOptions>(
}
return unref(visibility);
});
if (challenge.canStart == null) {
challenge.canStart = computed(
() =>
unref((challenge as GenericChallenge).visibility) === Visibility.Visible &&
Decimal.lt(
(challenge as GenericChallenge).completions.value,
unref((challenge as GenericChallenge).completionLimit)
)
);
}
if (challenge.canComplete == null) {
challenge.canComplete = computed(() => {
const genericChallenge = challenge as GenericChallenge;
@ -251,6 +261,34 @@ export function createChallenge<T extends ChallengeOptions>(
});
}
export function setupAutoComplete(
challenge: GenericChallenge,
autoActive: Computable<boolean> = true,
exitOnComplete = true
): WatchStopHandle {
const isActive = typeof autoActive === "function" ? computed(autoActive) : autoActive;
return watch([challenge.canComplete, isActive], ([canComplete, isActive]) => {
if (canComplete && isActive) {
challenge.complete(!exitOnComplete);
}
});
}
export function createActiveChallenge(
challenges: GenericChallenge[]
): Ref<GenericChallenge | undefined> {
return computed(() => challenges.find(challenge => challenge.active.value));
}
export function isAnyChallengeActive(
challenges: GenericChallenge[] | Ref<GenericChallenge | undefined>
): Ref<boolean> {
if (isArray(challenges)) {
challenges = createActiveChallenge(challenges);
}
return computed(() => (challenges as Ref<GenericChallenge | undefined>).value != null);
}
declare module "game/settings" {
interface Settings {
hideChallenges: boolean;

View file

@ -9,6 +9,8 @@ import {
StyleValue,
Visibility
} from "features/feature";
import { GenericLayer } from "game/layers";
import { Unsubscribe } from "nanoevents";
import {
Computable,
GetComputableType,
@ -17,7 +19,7 @@ import {
ProcessedComputable
} from "util/computed";
import { createLazyProxy } from "util/proxies";
import { unref } from "vue";
import { computed, unref } from "vue";
export const ClickableType = Symbol("Clickable");
@ -39,7 +41,7 @@ export interface ClickableOptions {
onHold?: VoidFunction;
}
interface BaseClickable {
export interface BaseClickable {
id: string;
type: typeof ClickableType;
[Component]: typeof ClickableComponent;
@ -131,3 +133,16 @@ export function createClickable<T extends ClickableOptions>(
return clickable as unknown as Clickable<T>;
});
}
export function setupAutoClick(
layer: GenericLayer,
clickable: GenericClickable,
autoActive: Computable<boolean> = true
): Unsubscribe {
const isActive = typeof autoActive === "function" ? computed(autoActive) : autoActive;
return layer.on("update", () => {
if (unref(isActive) && unref(clickable.canClick)) {
clickable.onClick?.();
}
});
}

View file

@ -23,7 +23,7 @@ export interface ConversionOptions {
modifyGainAmount?: (gain: DecimalSource) => DecimalSource;
}
interface BaseConversion {
export interface BaseConversion {
convert: VoidFunction;
}
@ -206,7 +206,7 @@ export function setupPassiveGeneration(
conversion: GenericConversion,
rate: ProcessedComputable<DecimalSource> = 1
): void {
layer.on("preUpdate", (diff: Decimal) => {
layer.on("preUpdate", diff => {
const currRate = isRef(rate) ? rate.value : rate;
if (Decimal.neq(currRate, 0)) {
conversion.gainResource.value = Decimal.add(

View file

@ -1,6 +1,7 @@
import { hasWon } from "data/projEntry";
import { globalBus } from "game/events";
import player from "game/player";
import { registerInfoComponent } from "game/settings";
import {
Computable,
GetComputableTypeWithDefault,
@ -9,10 +10,10 @@ import {
processComputable
} from "util/computed";
import { createLazyProxy } from "util/proxies";
import { unref } from "vue";
import { findFeatures, Replace, setDefault } from "./feature";
import { shallowReactive, unref } from "vue";
import { findFeatures, jsx, Replace, setDefault } from "./feature";
export const hotkeys: Record<string, GenericHotkey | undefined> = {};
export const hotkeys: Record<string, GenericHotkey | undefined> = shallowReactive({});
export const HotkeyType = Symbol("Hotkey");
export interface HotkeyOptions {
@ -22,7 +23,7 @@ export interface HotkeyOptions {
onPress: VoidFunction;
}
interface BaseHotkey {
export interface BaseHotkey {
type: typeof HotkeyType;
}
@ -88,3 +89,23 @@ document.onkeydown = function (e) {
hotkey.onPress();
}
};
registerInfoComponent(
jsx(() => {
const keys = Object.values(hotkeys).filter(hotkey => unref(hotkey?.enabled));
if (keys.length === 0) {
return "";
}
return (
<div>
<br />
<h4>Hotkeys</h4>
{keys.map(hotkey => (
<div>
{hotkey?.key}: {hotkey?.description}
</div>
))}
</div>
);
})
);

View file

@ -33,7 +33,7 @@ export interface InfoboxOptions {
display: Computable<CoercableComponent>;
}
interface BaseInfobox extends Persistent<boolean> {
export interface BaseInfobox extends Persistent<boolean> {
id: string;
collapsed: Ref<boolean>;
type: typeof InfoboxType;

View file

@ -2,7 +2,6 @@ import Select from "components/fields/Select.vue";
import {
CoercableComponent,
Component,
findFeatures,
GatherProps,
getUniqueID,
jsx,
@ -26,10 +25,11 @@ import {
} from "util/computed";
import { createLazyProxy } from "util/proxies";
import { coerceComponent, isCoercableComponent } from "util/vue";
import { Unsubscribe } from "nanoevents";
import { computed, Ref, unref } from "vue";
import { computed, Ref, unref, watchEffect } from "vue";
import { useToast } from "vue-toastification";
const toast = useToast();
export const MilestoneType = Symbol("Milestone");
export enum MilestoneDisplay {
@ -42,7 +42,7 @@ export enum MilestoneDisplay {
export interface MilestoneOptions {
visibility?: Computable<Visibility>;
shouldEarn: Computable<boolean>;
shouldEarn?: () => boolean;
style?: Computable<StyleValue>;
classes?: Computable<Record<string, boolean>>;
display?: Computable<
@ -56,9 +56,10 @@ export interface MilestoneOptions {
onComplete?: VoidFunction;
}
interface BaseMilestone extends Persistent<boolean> {
export interface BaseMilestone extends Persistent<boolean> {
id: string;
earned: Ref<boolean>;
complete: VoidFunction;
type: typeof MilestoneType;
[Component]: typeof MilestoneComponent;
[GatherProps]: () => Record<string, unknown>;
@ -68,7 +69,6 @@ export type Milestone<T extends MilestoneOptions> = Replace<
T & BaseMilestone,
{
visibility: GetComputableTypeWithDefault<T["visibility"], Visibility.Visible>;
shouldEarn: GetComputableType<T["shouldEarn"]>;
style: GetComputableType<T["style"]>;
classes: GetComputableType<T["classes"]>;
display: GetComputableType<T["display"]>;
@ -93,6 +93,10 @@ export function createMilestone<T extends MilestoneOptions>(
milestone[Component] = MilestoneComponent;
milestone.earned = milestone[PersistentState];
milestone.complete = function () {
milestone[PersistentState].value = true;
};
processComputable(milestone as T, "visibility");
setDefault(milestone, "visibility", Visibility.Visible);
const visibility = milestone.visibility as ProcessedComputable<Visibility>;
@ -124,7 +128,6 @@ export function createMilestone<T extends MilestoneOptions>(
}
});
processComputable(milestone as T, "shouldEarn");
processComputable(milestone as T, "style");
processComputable(milestone as T, "classes");
processComputable(milestone as T, "display");
@ -134,52 +137,40 @@ export function createMilestone<T extends MilestoneOptions>(
return { visibility, display, style, classes, earned, id };
};
if (milestone.shouldEarn) {
const genericMilestone = milestone as GenericMilestone;
watchEffect(() => {
if (
!genericMilestone.earned.value &&
unref(genericMilestone.visibility) === Visibility.Visible &&
genericMilestone.shouldEarn?.()
) {
genericMilestone.earned.value = true;
genericMilestone.onComplete?.();
if (genericMilestone.display) {
const display = unref(genericMilestone.display);
const Display = coerceComponent(
isCoercableComponent(display) ? display : display.requirement
);
toast(
<>
<h3>Milestone earned!</h3>
<div>
{/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */}
{/* @ts-ignore */}
<Display />
</div>
</>
);
}
}
});
}
return milestone as unknown as Milestone<T>;
});
}
const toast = useToast();
const listeners: Record<string, Unsubscribe | undefined> = {};
globalBus.on("addLayer", layer => {
const milestones: GenericMilestone[] = (
findFeatures(layer, MilestoneType) as GenericMilestone[]
).filter(milestone => milestone.shouldEarn != null);
listeners[layer.id] = layer.on("postUpdate", () => {
milestones.forEach(milestone => {
if (
unref(milestone.visibility) === Visibility.Visible &&
!milestone.earned.value &&
unref(milestone.shouldEarn)
) {
milestone[PersistentState].value = true;
milestone.onComplete?.();
if (milestone.display) {
const display = unref(milestone.display);
const Display = coerceComponent(
isCoercableComponent(display) ? display : display.requirement
);
toast(
<>
<h3>Milestone earned!</h3>
<div>
{/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */}
{/* @ts-ignore */}
<Display />
</div>
</>
);
}
}
});
});
});
globalBus.on("removeLayer", layer => {
// unsubscribe from postUpdate
listeners[layer.id]?.();
listeners[layer.id] = undefined;
});
declare module "game/settings" {
interface Settings {
msDisplay: MilestoneDisplay;

View file

@ -8,11 +8,11 @@ import {
PersistentRef,
PersistentState
} from "game/persistence";
import Decimal from "lib/break_eternity";
import Decimal from "util/bignum";
import { Computable, GetComputableType, processComputable } from "util/computed";
import { createLazyProxy } from "util/proxies";
import { Unsubscribe } from "nanoevents";
import { computed, isRef, unref } from "vue";
import { isRef, unref } from "vue";
export const ResetType = Symbol("Reset");
@ -21,7 +21,7 @@ export interface ResetOptions {
onReset?: VoidFunction;
}
interface BaseReset {
export interface BaseReset {
id: string;
reset: VoidFunction;
type: typeof ResetType;
@ -69,23 +69,10 @@ export function createReset<T extends ResetOptions>(
});
}
export function setupAutoReset(
layer: GenericLayer,
reset: GenericReset,
autoActive: Computable<boolean> = true
): Unsubscribe {
const isActive = typeof autoActive === "function" ? computed(autoActive) : autoActive;
return layer.on("update", () => {
if (unref(isActive)) {
reset.reset();
}
});
}
const listeners: Record<string, Unsubscribe | undefined> = {};
export function trackResetTime(layer: GenericLayer, reset: GenericReset): PersistentRef<Decimal> {
const resetTime = persistent<Decimal>(new Decimal(0));
listeners[layer.id] = layer.on("preUpdate", (diff: Decimal) => {
listeners[layer.id] = layer.on("preUpdate", diff => {
resetTime.value = Decimal.add(resetTime.value, diff);
});
globalBus.on("reset", currentReset => {

View file

@ -42,6 +42,8 @@ export function trackTotal(resource: Resource): Ref<DecimalSource> {
return total;
}
const tetra8 = new Decimal("10^^8");
const e100 = new Decimal("1e100");
export function trackOOMPS(
resource: Resource,
pointGain?: ComputedRef<DecimalSource>
@ -52,7 +54,7 @@ export function trackOOMPS(
globalBus.on("update", diff => {
oompsMag.value = 0;
if (Decimal.lte(resource.value, 1e100)) {
if (Decimal.lte(resource.value, e100)) {
lastPoints.value = resource.value;
return;
}
@ -61,7 +63,7 @@ export function trackOOMPS(
let prev = lastPoints.value;
lastPoints.value = curr;
if (Decimal.gt(curr, prev)) {
if (Decimal.gte(curr, "10^^8")) {
if (Decimal.gte(curr, tetra8)) {
curr = Decimal.slog(curr, 1e10);
prev = Decimal.slog(prev, 1e10);
oomps.value = curr.sub(prev).div(diff);

View file

@ -18,7 +18,7 @@ export interface TabOptions {
display: Computable<CoercableComponent>;
}
interface BaseTab {
export interface BaseTab {
id: string;
type: typeof TabType;
[Component]: typeof TabComponent;

View file

@ -34,7 +34,7 @@ export interface TabButtonOptions {
glowColor?: Computable<string>;
}
interface BaseTabButton {
export interface BaseTabButton {
type: typeof TabButtonType;
[Component]: typeof TabButtonComponent;
}
@ -65,7 +65,7 @@ export interface TabFamilyOptions {
style?: Computable<StyleValue>;
}
interface BaseTabFamily extends Persistent<string> {
export interface BaseTabFamily extends Persistent<string> {
id: string;
activeTab: Ref<GenericTab | CoercableComponent | null>;
selected: Ref<string>;

View file

@ -14,8 +14,7 @@ import { displayResource, Resource } from "features/resources/resource";
import { Tooltip } from "features/tooltip";
import TreeComponent from "features/trees/Tree.vue";
import { persistent } from "game/persistence";
import { DecimalSource, format } from "util/bignum";
import Decimal, { formatWhole } from "util/break_eternity";
import Decimal, { DecimalSource, format, formatWhole } from "util/bignum";
import {
Computable,
convertComputable,
@ -137,7 +136,7 @@ export interface TreeOptions {
onReset?: (node: GenericTreeNode) => void;
}
interface BaseTree {
export interface BaseTree {
id: string;
links: Ref<Link[]>;
reset: (node: GenericTreeNode) => void;

View file

@ -31,7 +31,7 @@ import MarkNode from "components/MarkNode.vue";
import { jsx, StyleValue, Visibility } from "features/feature";
import { displayResource, Resource } from "features/resources/resource";
import { GenericUpgrade } from "features/upgrades/upgrade";
import { DecimalSource } from "lib/break_eternity";
import { DecimalSource } from "util/bignum";
import { coerceComponent, isCoercableComponent, processedPropType, unwrapRef } from "util/vue";
import {
Component,

View file

@ -46,7 +46,7 @@ export interface UpgradeOptions {
onPurchase?: VoidFunction;
}
interface BaseUpgrade extends Persistent<boolean> {
export interface BaseUpgrade extends Persistent<boolean> {
id: string;
bought: Ref<boolean>;
canPurchase: Ref<boolean>;

View file

@ -1,7 +1,7 @@
import projInfo from "data/projInfo.json";
import Decimal, { DecimalSource } from "util/bignum";
import Decimal from "util/bignum";
import { createNanoEvents } from "nanoevents";
import { App, Ref } from "vue";
import { App, Ref, watch } from "vue";
import { GenericLayer } from "./layers";
import player from "./player";
import settings, { Settings } from "./settings";
@ -10,8 +10,9 @@ import state from "./state";
export interface GlobalEvents {
addLayer: (layer: GenericLayer, saveData: Record<string, unknown>) => void;
removeLayer: (layer: GenericLayer) => void;
update: (diff: Decimal, trueDiff: number) => void;
update: (diff: number, trueDiff: number) => void;
loadSettings: (settings: Partial<Settings>) => void;
gameWon: VoidFunction;
setupVue: (vue: App) => void;
}
@ -25,7 +26,7 @@ let hasWon: null | Ref<boolean> = null;
function update() {
const now = Date.now();
let diff: DecimalSource = (now - player.time) / 1e3;
let diff = (now - player.time) / 1e3;
player.time = now;
const trueDiff = diff;
@ -43,7 +44,7 @@ function update() {
return;
}
diff = new Decimal(diff).max(0);
diff = Math.max(diff, 0);
if (player.devSpeed === 0) {
return;
@ -52,14 +53,14 @@ function update() {
// Add offline time if any
if (player.offlineTime != undefined) {
if (Decimal.gt(player.offlineTime, projInfo.offlineLimit * 3600)) {
player.offlineTime = new Decimal(projInfo.offlineLimit * 3600);
player.offlineTime = projInfo.offlineLimit * 3600;
}
if (Decimal.gt(player.offlineTime, 0) && player.devSpeed !== 0) {
const offlineDiff = Decimal.div(player.offlineTime, 10).max(diff);
player.offlineTime = Decimal.sub(player.offlineTime, offlineDiff);
diff = diff.add(offlineDiff);
const offlineDiff = Math.max(player.offlineTime / 10, diff);
player.offlineTime = player.offlineTime - offlineDiff;
diff += offlineDiff;
} else if (player.devSpeed === 0) {
player.offlineTime = Decimal.add(player.offlineTime, diff);
player.offlineTime += diff;
}
if (!player.offlineProd || Decimal.lt(player.offlineTime, 0)) {
player.offlineTime = null;
@ -67,18 +68,26 @@ function update() {
}
// Cap at max tick length
diff = Decimal.min(diff, projInfo.maxTickLength);
diff = Math.min(diff, projInfo.maxTickLength);
// Apply dev speed
if (player.devSpeed != undefined) {
diff = diff.times(player.devSpeed);
diff *= player.devSpeed;
}
if (!Number.isFinite(diff)) {
diff = 1e308;
}
// Update
if (diff.eq(0)) {
if (Decimal.eq(diff, 0)) {
return;
}
player.timePlayed = Decimal.add(player.timePlayed, diff);
player.timePlayed += diff;
if (!Number.isFinite(player.timePlayed)) {
player.timePlayed = 1e308;
}
globalBus.emit("update", diff, trueDiff);
if (settings.unthrottled) {
@ -94,6 +103,11 @@ function update() {
export async function startGameLoop() {
hasWon = (await import("data/projEntry")).hasWon;
watch(hasWon, hasWon => {
if (hasWon) {
globalBus.emit("gameWon");
}
});
if (settings.unthrottled) {
requestAnimationFrame(update);
} else {

View file

@ -8,7 +8,6 @@ import {
StyleValue
} from "features/feature";
import { Link } from "features/links";
import Decimal from "util/bignum";
import {
Computable,
GetComputableType,
@ -25,11 +24,11 @@ import player from "./player";
export interface LayerEvents {
// Generation
preUpdate: (diff: Decimal) => void;
preUpdate: (diff: number) => void;
// Actions (e.g. automation)
update: (diff: Decimal) => void;
update: (diff: number) => void;
// Effects (e.g. milestones)
postUpdate: (diff: Decimal) => void;
postUpdate: (diff: number) => void;
}
export const layers: Record<string, Readonly<GenericLayer> | undefined> = {};

View file

@ -1,4 +1,4 @@
import Decimal, { DecimalSource } from "util/bignum";
import Decimal from "util/bignum";
import { isPlainObject } from "util/common";
import { ProxiedWithState, ProxyPath, ProxyState } from "util/proxies";
import { reactive, unref } from "vue";
@ -6,14 +6,14 @@ import transientState from "./state";
export interface PlayerData {
id: string;
devSpeed: DecimalSource | null;
devSpeed: number | null;
name: string;
tabs: Array<string>;
time: number;
autosave: boolean;
offlineProd: boolean;
offlineTime: DecimalSource | null;
timePlayed: DecimalSource;
offlineTime: number | null;
timePlayed: number;
keepGoing: boolean;
modID: string;
modVersion: string;
@ -31,7 +31,7 @@ const state = reactive<PlayerData>({
autosave: true,
offlineProd: true,
offlineTime: null,
timePlayed: new Decimal(0),
timePlayed: 0,
keepGoing: false,
modID: "",
modVersion: "",

View file

@ -59,7 +59,11 @@ export const hardResetSettings = (window.hardResetSettings = () => {
});
export const settingFields: CoercableComponent[] = reactive([]);
export function registerSettingField(component: CoercableComponent) {
settingFields.push(component);
}
export const infoComponents: CoercableComponent[] = reactive([]);
export function registerInfoComponent(component: CoercableComponent) {
infoComponents.push(component);
}

1
src/lib/vue-panzoom.d.ts vendored Normal file
View file

@ -0,0 +1 @@
declare module 'vue-panzoom';

1
src/lib/vue-textarea-autosize.d.ts vendored Normal file
View file

@ -0,0 +1 @@
declare module 'vue-textarea-autosize';

View file

@ -5,7 +5,7 @@ import { GenericLayer } from "./game/layers";
import { PlayerData } from "./game/player";
import { Settings } from "./game/settings";
import { Transient } from "./game/state";
import Decimal, { DecimalSource } from "./lib/break_eternity";
import Decimal, { DecimalSource } from "./util/bignum";
import { load } from "./util/save";
document.title = projInfo.title;

View file

@ -15,9 +15,9 @@ export type GetComputableType<T> = T extends { [DoNotCache]: true }
export type GetComputableTypeWithDefault<T, S> = undefined extends T
? S
: GetComputableType<NonNullable<T>>;
type UnwrapComputableType<T> = T extends Ref<infer S> ? S : T extends () => infer S ? S : T;
export type UnwrapComputableType<T> = T extends Ref<infer S> ? S : T extends () => infer S ? S : T;
type ComputableKeysOf<T> = Pick<
export type ComputableKeysOf<T> = Pick<
T,
{
[K in keyof T]: T[K] extends Computable<unknown> ? K : never;

View file

@ -1,7 +1,6 @@
import projInfo from "data/projInfo.json";
import player, { Player, PlayerData, stringifySave } from "game/player";
import settings, { loadSettings } from "game/settings";
import Decimal from "./bignum";
import { ProxyState } from "./proxies";
export function setupInitialStore(player: Partial<PlayerData> = {}): Player {
@ -13,8 +12,8 @@ export function setupInitialStore(player: Partial<PlayerData> = {}): Player {
time: Date.now(),
autosave: true,
offlineProd: true,
offlineTime: new Decimal(0),
timePlayed: new Decimal(0),
offlineTime: 0,
timePlayed: 0,
keepGoing: false,
modID: projInfo.id,
modVersion: projInfo.versionNumber,
@ -85,11 +84,8 @@ export async function loadSave(playerObj: Partial<PlayerData>): Promise<void> {
playerObj = setupInitialStore(playerObj);
if (playerObj.offlineProd && playerObj.time) {
if (playerObj.offlineTime == undefined) playerObj.offlineTime = new Decimal(0);
playerObj.offlineTime = Decimal.add(
playerObj.offlineTime,
(Date.now() - playerObj.time) / 1000
);
if (playerObj.offlineTime == undefined) playerObj.offlineTime = 0;
playerObj.offlineTime += (Date.now() - playerObj.time) / 1000;
}
playerObj.time = Date.now();
if (playerObj.modVersion !== projInfo.versionNumber) {

View file

@ -14,6 +14,7 @@ import {
DefineComponent,
defineComponent,
isRef,
onUnmounted,
PropType,
ref,
Ref,
@ -113,6 +114,8 @@ export function setupHoldToClick(
}
}
onUnmounted(stop);
return { start, stop, handleHolding };
}
@ -154,7 +157,7 @@ export function setRefValue<T>(ref: Ref<T | Ref<T>>, value: T) {
}
}
type PropTypes =
export type PropTypes =
| typeof Boolean
| typeof String
| typeof Number

View file

@ -35,5 +35,15 @@
],
"exclude": [
"node_modules"
]
],
"typedocOptions": {
"entryPoints": ["src"],
"entryPointStrategy": "expand",
"cleanOutputDir": true,
"name": "Profectus",
"includeVersion": true,
"categorizeByGroup": false,
"readme": "none",
"out": "../docs/api"
}
}