diff --git a/evolve_analytics.meta.js b/evolve_analytics.meta.js index ebe6935..8906389 100644 --- a/evolve_analytics.meta.js +++ b/evolve_analytics.meta.js @@ -1,7 +1,7 @@ // ==UserScript== // @name Evolve Analytics // @namespace http://tampermonkey.net/ -// @version 0.9.3 +// @version 0.10.0 // @description Track and see detailed information about your runs // @author Sneed // @match https://pmotschmann.github.io/Evolve/ diff --git a/package-lock.json b/package-lock.json index f05c42d..f7cdb43 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1944,10 +1944,11 @@ } }, "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { "path-key": "^3.1.0", diff --git a/src/config.ts b/src/config.ts index b1a3e49..0c2aa48 100644 --- a/src/config.ts +++ b/src/config.ts @@ -7,6 +7,7 @@ export type ViewConfig = { resetType: keyof typeof resets, universe?: keyof typeof universes, mode: keyof typeof viewModes, + includeCurrentRun?: boolean, smoothness: number, showBars: boolean, showLines: boolean, @@ -129,7 +130,13 @@ export class ConfigManager extends Subscribable { return this.config.lastOpenViewIndex; } - onViewOpened(view: View) { + get openView() { + if (this.openViewIndex !== undefined) { + return this.views[this.openViewIndex]; + } + } + + viewOpened(view: View) { const idx = this.views.indexOf(view); this.config.lastOpenViewIndex = idx === -1 ? undefined : idx; @@ -141,6 +148,7 @@ export class ConfigManager extends Subscribable { const view: ViewConfig = { resetType: "ascend", universe: this.game.universe, + includeCurrentRun: false, mode: "timestamp", showBars: true, showLines: false, diff --git a/src/exports/plotPoints.ts b/src/exports/plotPoints.ts index e287d4b..ec9b03a 100644 --- a/src/exports/plotPoints.ts +++ b/src/exports/plotPoints.ts @@ -1,7 +1,8 @@ import { generateMilestoneNames } from "../milestones"; -import { transformMap, zip } from "../utils"; -import type { HistoryManager, HistoryEntry, MilestoneReference } from "../history"; +import { zip } from "../utils"; +import type { HistoryManager, HistoryEntry } from "../history"; import type { ViewConfig } from "../config"; +import type { LatestRun } from "../runTracking"; export type PlotPoint = { run: number, @@ -9,7 +10,8 @@ export type PlotPoint = { day: number, dayDiff?: number, // days since the last enabled non-event milestone segment: number, // days since the last non-event milestone - raceName?: string + raceName?: string, + pending?: boolean } function makeMilestoneNamesMapping(history: HistoryManager, view: ViewConfig): Record { @@ -20,71 +22,135 @@ function makeMilestoneNamesMapping(history: HistoryManager, view: ViewConfig): R return Object.fromEntries(zip(milestoneIDs, milestoneNames)); } -export function asPlotPoints(filteredRuns: HistoryEntry[], history: HistoryManager, view: ViewConfig): PlotPoint[] { - const milestones = transformMap(view.milestones, ([milestone, enabled]) => { - return [history.getMilestoneID(milestone), { enabled, isEvent: milestone.startsWith("event:") }]; - }); +class SegmentCounter { + private previousDay = 0; + private previousEnabledDay = 0; - const milestoneNames = makeMilestoneNamesMapping(history, view); + constructor(private view: ViewConfig) {} - const entries: PlotPoint[] = []; + reset() { + this.previousDay = 0; + this.previousEnabledDay = 0; + } - for (let i = 0; i !== filteredRuns.length; ++i) { - const run = filteredRuns[i]; + next(milestone: string, day: number) { + const dayDiff = day - this.previousEnabledDay; + const segment = day - this.previousDay; - // Events have their separate segmentation logic - const events: MilestoneReference[] = []; - const nonEvents: MilestoneReference[] = []; + const isEvent = milestone.startsWith("event:"); + const enabled = this.view.milestones[milestone]; - for (const [milestoneID, day] of run.milestones) { - if (!(milestoneID in milestones)) { - continue; - } + if (!isEvent) { + this.previousDay = day; - if (milestones[milestoneID].isEvent) { - events.push([milestoneID, day]); - } - else { - nonEvents.push([milestoneID, day]); + if (enabled) { + this.previousEnabledDay = day; } } - for (const [milestoneID, day] of events) { - if (!milestones[milestoneID].enabled) { - continue; - } + if (enabled) { + return { + dayDiff: isEvent ? undefined : dayDiff, + segment: isEvent ? day : segment + }; + } + } +} - entries.push({ - run: i, - raceName: run.raceName, - milestone: milestoneNames[milestoneID], - day, - segment: day - }); +export function runAsPlotPoints(currentRun: LatestRun, view: ViewConfig, runIdx: number, orderedMilestones: string[]): PlotPoint[] { + const milestoneNames = generateMilestoneNames(orderedMilestones, view.universe); + + const entries: PlotPoint[] = []; + + const counter = new SegmentCounter(view); + + let nextMilestoneIdx = 0; + for (let i = 0; i !== orderedMilestones.length; ++i) { + const milestone = orderedMilestones[i]; + const milestoneName = milestoneNames[i]; + + const day = currentRun.milestones[milestone]; + if (day === undefined) { + continue; } - let previousDay = 0; - let previousEnabledDay = 0; + nextMilestoneIdx = i + 1; - for (const [milestoneID, day] of nonEvents) { - const dayDiff = day - previousEnabledDay; - const segment = day - previousDay; + const info = counter.next(milestone, day); + if (info === undefined) { + continue; + } + + entries.push({ + run: runIdx, + raceName: currentRun.raceName, + milestone: milestoneName, + day, + dayDiff: info.dayDiff, + segment: info.segment + }); + } + + // Guess what the next milestone is gonna be, default to the view's reset + let milestone = `reset:${view.resetType}`; + for (; nextMilestoneIdx !== orderedMilestones.length; ++nextMilestoneIdx) { + const candidate = orderedMilestones[nextMilestoneIdx]; + if (!candidate.startsWith("event:") && view.milestones[candidate]) { + milestone = candidate; + break; + } + } + + const info = counter.next(milestone, currentRun.totalDays); + if (info === undefined) { + return entries; + } + + entries.push({ + run: runIdx, + raceName: currentRun.raceName, + milestone: generateMilestoneNames([milestone], view.universe)[0], + day: currentRun.totalDays, + dayDiff: info.dayDiff, + segment: info.segment, + pending: true + }); + + return entries; +} - previousDay = day; +export function asPlotPoints(filteredRuns: HistoryEntry[], history: HistoryManager, view: ViewConfig): PlotPoint[] { + const milestoneNames = makeMilestoneNamesMapping(history, view); + + const entries: PlotPoint[] = []; - if (!milestones[milestoneID].enabled) { + const counter = new SegmentCounter(view); + + for (let i = 0; i !== filteredRuns.length; ++i) { + const run = filteredRuns[i]; + + counter.reset(); + + for (const [milestoneID, day] of run.milestones) { + const milestone = history.getMilestone(milestoneID); + const milestoneName = milestoneNames[milestoneID]; + + if (!(milestone in view.milestones)) { continue; } - previousEnabledDay = day; + const info = counter.next(milestone, day); + if (info === undefined) { + continue; + } entries.push({ run: i, raceName: run.raceName, - milestone: milestoneNames[milestoneID], + milestone: milestoneName, day, - dayDiff, - segment + dayDiff: info.dayDiff, + segment: info.segment }); } } diff --git a/src/game.ts b/src/game.ts index 6d73887..fbf154a 100644 --- a/src/game.ts +++ b/src/game.ts @@ -1,13 +1,18 @@ import { resets } from "./enums"; import { transformMap } from "./utils"; +import { Subscribable } from "./subscribable"; import type { Evolve, BuildingInfoTabs, ArpaInfoTab } from "./evolve"; import type { default as JQuery } from "jquery"; declare const $: typeof JQuery; -export class Game { - constructor(private evolve: Evolve) {} +export class Game extends Subscribable { + private subscribed = false; + + constructor(private evolve: Evolve) { + super(); + } get runNumber() { return this.evolve.global.stats.reset + 1; @@ -53,12 +58,22 @@ export class Game { } onGameDay(fn: (day: number) => void) { + this.on("newDay", fn); + + if (!this.subscribed) { + this.subscribeToGameUpdates(); + this.subscribed = true; + } + } + + private subscribeToGameUpdates() { let previousDay: number | null = null; this.onGameTick(() => { const day = this.day; if (previousDay !== day) { - fn(day); + this.emit("newDay", this.day); + previousDay = day; } }); diff --git a/src/index.ts b/src/index.ts index 8ce1b95..63618dd 100644 --- a/src/index.ts +++ b/src/index.ts @@ -15,6 +15,6 @@ const history = initializeHistory(game, config); processLatestRun(game, config, history); -trackMilestones(game, config); +const currentRun = trackMilestones(game, config); -bootstrapUIComponents(config, history); +bootstrapUIComponents(game, config, history, currentRun); diff --git a/src/milestones.ts b/src/milestones.ts index 430e2d4..ee0d5ad 100644 --- a/src/milestones.ts +++ b/src/milestones.ts @@ -35,7 +35,7 @@ export function milestoneName(milestone: string, universe?: keyof typeof univers export function generateMilestoneNames(milestones: string[], universe?: keyof typeof universes): string[] { const candidates: Record = {}; - for (let i = 0; i != milestones.length; ++i) { + for (let i = 0; i !== milestones.length; ++i) { const [name, discriminator, force] = milestoneName(milestones[i], universe); (candidates[name] ??= []).push([i, discriminator, force]); } diff --git a/src/runTracking.ts b/src/runTracking.ts index 6802932..610be70 100644 --- a/src/runTracking.ts +++ b/src/runTracking.ts @@ -114,4 +114,6 @@ export function trackMilestones(game: Game, config: ConfigManager) { saveCurrentRun(currentRunStats); }); + + return currentRunStats; } diff --git a/src/ui/analyticsTab.ts b/src/ui/analyticsTab.ts index 2c7bd0f..8cefa84 100644 --- a/src/ui/analyticsTab.ts +++ b/src/ui/analyticsTab.ts @@ -1,10 +1,11 @@ import { makeViewTab } from "./viewTab"; import { lastChild } from "./utils"; -import { weakFor, invokeFor, compose } from "../utils"; +import type { Game } from "../game"; import type { ConfigManager, View } from "../config"; import type { HistoryManager } from "../history"; +import type { LatestRun } from "../runTracking"; -export function buildAnalyticsTab(config: ConfigManager, history: HistoryManager) { +export function buildAnalyticsTab(game: Game, config: ConfigManager, history: HistoryManager, currentRun: LatestRun) { const tabControlNode = $(`
  • Analytics @@ -38,10 +39,10 @@ export function buildAnalyticsTab(config: ConfigManager, history: HistoryManager const count = controlParentNode.children().length; const id = `analytics-view-${count}`; - const [controlNode, contentNode] = makeViewTab(id, view, config, history); + const [controlNode, contentNode] = makeViewTab(id, game, view, config, history, currentRun); controlNode.on("click", () => { - config.onViewOpened(view); + config.viewOpened(view); }); controlNode.insertBefore(lastChild(analyticsPanel.find("> nav > ul"))); @@ -49,12 +50,16 @@ export function buildAnalyticsTab(config: ConfigManager, history: HistoryManager analyticsPanel.tabs("refresh"); analyticsPanel.tabs({ active: count - 1 }); - config.on("viewRemoved", compose([weakFor(view), invokeFor(view)], () => { + config.on("viewRemoved", (removedView) => { + if (removedView !== view) { + return; + } + controlNode.remove(); contentNode.remove(); analyticsPanel.tabs("refresh"); analyticsPanel.tabs({ active: 0 }); - })); + }); } function hidden(node: JQuery) { diff --git a/src/ui/graph.ts b/src/ui/graph.ts index 93b1313..903fc39 100644 --- a/src/ui/graph.ts +++ b/src/ui/graph.ts @@ -1,8 +1,9 @@ import { applyFilters } from "../exports/historyFiltering"; -import { asPlotPoints, type PlotPoint } from "../exports/plotPoints"; +import { asPlotPoints, runAsPlotPoints, type PlotPoint } from "../exports/plotPoints"; import { generateMilestoneNames } from "../milestones"; import type { View } from "../config"; import type { HistoryEntry, HistoryManager } from "../history"; +import type { LatestRun } from "../runTracking"; import type { default as PlotType } from "@observablehq/plot"; @@ -12,7 +13,7 @@ function calculateYScale(plotPoints: PlotPoint[], view: View): [number, number] if (view.daysScale) { return [0, view.daysScale]; } - else if (plotPoints.length === 0) { + else if (plotPoints.length === 0 || (!view.showBars && !view.showLines)) { // Default scale with empty history return [0, 1000]; } @@ -59,7 +60,9 @@ function smooth(smoothness: number, history: HistoryEntry[], params: any) { } function* timestamps(plotPoints: PlotPoint[], key: "day" | "segment") { - const lastRunTimestamps = lastRunEntries(plotPoints).map(e => e[key]); + const lastRunTimestamps = lastRunEntries(plotPoints) + .filter(entry => !entry.pending) + .map(entry => entry[key]); yield Plot.axisY(lastRunTimestamps, { anchor: "right", @@ -73,7 +76,8 @@ function* areaMarks(plotPoints: PlotPoint[], history: HistoryEntry[], smoothness y: "dayDiff", z: "milestone", fill: "milestone", - fillOpacity: 0.5 + fillOpacity: 0.5, + filter: (entry: PlotPoint) => entry.dayDiff !== undefined })); } @@ -122,13 +126,27 @@ function* barMarks(plotPoints: PlotPoint[], key: "dayDiff" | "segment") { } function tipText(point: PlotPoint, key: "day" | "dayDiff" | "segment", history: HistoryEntry[]) { - let prefix = `Run #${history[point.run].run}`; + let prefix; + if (point.run === history.length) { + prefix = "Current run"; + } + else { + prefix = `Run #${history[point.run].run}`; + } if (point.raceName !== undefined) { prefix += ` (${point.raceName})`; } - return `${prefix}: ${point.milestone} in ${point[key]} day(s)`; + let suffix; + if (point.pending) { + suffix = `(in progress)`; + } + else { + suffix = `in ${point[key]} day(s)`; + } + + return `${prefix}: ${point.milestone} ${suffix}`; } function* linePointerMarks(plotPoints: PlotPoint[], history: HistoryEntry[], key: "day" | "segment") { @@ -154,8 +172,6 @@ function* linePointerMarks(plotPoints: PlotPoint[], history: HistoryEntry[], key } function* rectPointerMarks(plotPoints: PlotPoint[], history: HistoryEntry[], segmentKey: "dayDiff" | "segment", tipKey: "day" | "segment") { - plotPoints = plotPoints.filter((entry: PlotPoint) => entry.dayDiff !== undefined); - // Transform pointer position from the point to the segment function toSegment(options: any) { const convert = ({ x, y, ...options }: any) => ({ px: x, py: y, ...options }); @@ -167,21 +183,41 @@ function* rectPointerMarks(plotPoints: PlotPoint[], history: HistoryEntry[], seg y: segmentKey, dy: -17, frameAnchor: "top-left", - text: (p: PlotPoint) => tipText(p, tipKey, history) + text: (p: PlotPoint) => tipText(p, tipKey, history), + filter: (entry: PlotPoint) => entry.dayDiff !== undefined }))); yield Plot.barY(plotPoints, Plot.pointerX(Plot.stackY({ x: "run", y: segmentKey, fill: "milestone", - fillOpacity: 0.5 + fillOpacity: 0.5, + filter: (entry: PlotPoint) => entry.dayDiff !== undefined }))); } -export function makeGraph(history: HistoryManager, view: View, onSelect: (run: HistoryEntry | null) => void) { +export function makeGraph(history: HistoryManager, view: View, currentRun: LatestRun, onSelect: (run: HistoryEntry | null) => void) { const filteredRuns = applyFilters(history, view); + + const milestones: string[] = Object.keys(view.milestones); + + // Try to order the milestones in the legend in the order in which they happened during the last run + if (filteredRuns.length !== 0) { + const lastRun = filteredRuns[filteredRuns.length - 1]; + milestones.sort((l, r) => { + const lIdx = lastRun.milestones.findIndex(([id]) => id === history.getMilestoneID(l)); + const rIdx = lastRun.milestones.findIndex(([id]) => id === history.getMilestoneID(r)); + return rIdx - lIdx; + }); + } + const plotPoints = asPlotPoints(filteredRuns, history, view); + if (view.includeCurrentRun) { + const currentRunPoints = runAsPlotPoints(currentRun, view, filteredRuns.length, milestones.slice().reverse()); + plotPoints.push(...currentRunPoints); + } + const marks = [ Plot.axisY({ anchor: "left", label: "days" }), Plot.ruleY([0]) @@ -232,18 +268,6 @@ export function makeGraph(history: HistoryManager, view: View, onSelect: (run: H } } - const milestones: string[] = Object.keys(view.milestones); - - // Try to order the milestones in the legend in the order in which they happened during the last run - if (filteredRuns.length !== 0) { - const lastRun = filteredRuns[filteredRuns.length - 1]; - milestones.sort((l, r) => { - const lIdx = lastRun.milestones.findIndex(([id]) => id === history.getMilestoneID(l)); - const rIdx = lastRun.milestones.findIndex(([id]) => id === history.getMilestoneID(r)); - return rIdx - lIdx; - }); - } - const plot = Plot.plot({ width: 800, x: { axis: null }, @@ -253,7 +277,7 @@ export function makeGraph(history: HistoryManager, view: View, onSelect: (run: H }); plot.addEventListener("mousedown", () => { - if (plot.value) { + if (plot.value && plot.value.run < filteredRuns.length) { onSelect(filteredRuns[plot.value.run]); } else { @@ -267,7 +291,7 @@ export function makeGraph(history: HistoryManager, view: View, onSelect: (run: H .css("cursor", "pointer") .css("font-size", "1rem"); - for (let i = 0; i != legendMilestones.length; ++i) { + for (let i = 0; i !== legendMilestones.length; ++i) { const node = legendMilestones[i]; const milestone = milestones[i]; diff --git a/src/ui/index.ts b/src/ui/index.ts index a5ca35a..6fb9fdb 100644 --- a/src/ui/index.ts +++ b/src/ui/index.ts @@ -1,8 +1,10 @@ import styles from "./styles.css"; import { buildAnalyticsTab } from "./analyticsTab"; import { waitFor, makeToggle } from "./utils"; +import type { Game } from "../game"; import type { ConfigManager } from "../config"; import type { HistoryManager } from "../history"; +import type { LatestRun } from "../runTracking"; function addMainToggle(config: ConfigManager) { waitFor("#settings").then(() => { @@ -11,10 +13,10 @@ function addMainToggle(config: ConfigManager) { }); } -export function bootstrapUIComponents(config: ConfigManager, history: HistoryManager) { +export function bootstrapUIComponents(game: Game, config: ConfigManager, history: HistoryManager, currentRun: LatestRun) { $("head").append(``); addMainToggle(config); - buildAnalyticsTab(config, history); + buildAnalyticsTab(game, config, history, currentRun); } diff --git a/src/ui/utils.ts b/src/ui/utils.ts index e686e27..37a0317 100644 --- a/src/ui/utils.ts +++ b/src/ui/utils.ts @@ -159,15 +159,17 @@ export function makeToggleableNumberInput( defaultValue: number | undefined, onStateChange: (value: number | undefined) => void ) { + const enabled = defaultValue !== undefined; + const inputNode = makeNumberInput(placeholder, defaultValue) .on("change", function(this: HTMLInputElement) { onStateChange(Number(this.value)); }); - const toggleNode = makeCheckbox(label, defaultValue !== undefined, value => { + const toggleNode = makeCheckbox(label, enabled, value => { inputNode.prop("disabled", !value); onStateChange(value ? Number(inputNode.val()) : undefined); }); - inputNode.prop("disabled", !toggleNode.val()); + inputNode.prop("disabled", !enabled); return $(`
    `) .append(toggleNode) diff --git a/src/ui/viewSettings.ts b/src/ui/viewSettings.ts index c541fff..54f5571 100644 --- a/src/ui/viewSettings.ts +++ b/src/ui/viewSettings.ts @@ -62,6 +62,8 @@ export function makeViewSettings(view: View) { const numRunsInput = makeToggleableNumberInput("Limit to last N runs", "All", view.numRuns, bind("numRuns")); + const showCurrentRunToggle = makeCheckbox("Show Current Run", view.includeCurrentRun ?? false, bind("includeCurrentRun")); + const modeInput = makeSelect(Object.entries(viewModes), view.mode) .on("change", bindThis("mode")); @@ -92,7 +94,8 @@ export function makeViewSettings(view: View) { const filterSettings = $(`
    `) .append(makeSetting("Reset type", resetTypeInput)) .append(makeSetting("Universe", universeInput)) - .append(numRunsInput); + .append(numRunsInput) + .append(showCurrentRunToggle); const displaySettings = $(`
    `) .append(makeSetting("Mode", modeInput)) diff --git a/src/ui/viewTab.ts b/src/ui/viewTab.ts index 5aae47b..399e7ec 100644 --- a/src/ui/viewTab.ts +++ b/src/ui/viewTab.ts @@ -1,12 +1,13 @@ import { resets, universes } from "../enums"; -import { weakFor, invokeFor, compose } from "../utils"; import { makeGraph } from "./graph"; import { makeViewSettings } from "./viewSettings"; import { makeMilestoneSettings } from "./milestoneSettings"; import { makeAdditionalInfoSettings } from "./additionalInfoSettings"; import { nextAnimationFrame } from "./utils"; +import type { Game } from "../game"; import type { ConfigManager, View } from "../config"; import type { HistoryEntry, HistoryManager } from "../history"; +import type { LatestRun } from "../runTracking"; import type { default as htmltoimage } from "html-to-image"; @@ -76,7 +77,7 @@ function viewTitle(view: View) { } } -export function makeViewTab(id: string, view: View, config: ConfigManager, history: HistoryManager) { +export function makeViewTab(id: string, game: Game, view: View, config: ConfigManager, history: HistoryManager, currentRun: LatestRun) { const controlNode = $(`
  • ${viewTitle(view)}
  • `); const contentNode = $(`
    `); @@ -116,14 +117,38 @@ export function makeViewTab(id: string, view: View, config: ConfigManager, histo .append(makeViewSettings(view).css("margin-bottom", "1em")) .append(makeAdditionalInfoSettings(view).css("margin-bottom", "1em")) .append(makeMilestoneSettings(view).css("margin-bottom", "1em")) - .append(makeGraph(history, view, onRunSelection)) + .append(makeGraph(history, view, currentRun, onRunSelection)) .append(buttonsContainerNode); - config.on("viewUpdated", compose([weakFor(view), invokeFor(view)], (updatedView) => { + function redrawGraph(updatedView: View) { + contentNode.find("figure:last").replaceWith(makeGraph(history, updatedView, currentRun, onRunSelection)); + } + + config.on("viewUpdated", (updatedView) => { + if (updatedView !== view) { + return; + } + controlNode.find("> a").text(viewTitle(updatedView)); - contentNode.find("figure:last").replaceWith(makeGraph(history, updatedView, onRunSelection)); + redrawGraph(updatedView); onRunSelection(null); - })); + }); + + game.onGameDay(() => { + if (!config.recordRuns) { + return; + } + + if (!view.includeCurrentRun) { + return; + } + + if (view !== config.openView) { + return; + } + + redrawGraph(view); + }); return [controlNode, contentNode]; } diff --git a/src/utils/functional.ts b/src/utils/functional.ts deleted file mode 100644 index 50970b1..0000000 --- a/src/utils/functional.ts +++ /dev/null @@ -1,54 +0,0 @@ -type Callback = (...args: any[]) => void; -type Decorator = (callback: Callback) => Callback; - -class WeakCallback { - static references = new WeakMap(); - private impl: WeakRef; - - constructor(key: WeakKey, impl: Callback) { - (WeakCallback.references.get(key) ?? WeakCallback.references.set(key, []).get(key)!).push(impl); - this.impl = new WeakRef(impl); - } - - call(arg: any) { - this.impl.deref()?.(arg); - } -} - -export function weakFor(ref: WeakKey): Decorator; -export function weakFor(ref: WeakKey, callback: Callback): Callback; -export function weakFor(ref: WeakKey, callback?: Callback): Callback { - if (callback !== undefined) { - const wrapper = new WeakCallback(ref, callback); - return (arg: any) => wrapper.call(arg); - } - else { - return (callback) => weakFor(ref, callback); - } -} - -export function invokeFor(ref: WeakKey): Decorator; -export function invokeFor(ref: WeakKey, callback: Callback): Callback; -export function invokeFor(ref: WeakKey, callback?: Callback): Callback { - if (callback !== undefined) { - const key = new WeakRef(ref); - return (arg: any) => { - if (arg === key.deref()) { - callback(arg); - } - }; - } - else { - return (callback) => invokeFor(ref, callback); - } -} - -export function compose(decorators: Decorator[], callback: Callback) { - let wrapper = callback; - - for (const decorator of decorators) { - wrapper = decorator(callback); - } - - return wrapper; -} diff --git a/src/utils/index.ts b/src/utils/index.ts index dd7e803..33675a5 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,5 +1,4 @@ export { transformMap, rotateMap, zip } from "./map"; -export { compose, invokeFor, weakFor } from "./functional"; export type RecursivePartial = { [P in keyof T]?: diff --git a/src/utils/map.ts b/src/utils/map.ts index b569ffb..46d3523 100644 --- a/src/utils/map.ts +++ b/src/utils/map.ts @@ -15,7 +15,7 @@ export function zip(...lists: [...T]) { const length = Math.min(...lists.map(l => l.length)); - for (let i = 0; i != length; ++i) { + for (let i = 0; i !== length; ++i) { result.push(lists.map(l => l[i])); } diff --git a/test/config.test.ts b/test/config.test.ts index 992bffa..c85a7c1 100644 --- a/test/config.test.ts +++ b/test/config.test.ts @@ -110,6 +110,7 @@ describe("Config", () => { smoothness: 0, resetType: "ascend", universe: "standard", + includeCurrentRun: false, milestones: { "reset:ascend": true }, @@ -307,10 +308,10 @@ describe("Config", () => { expect(config.openViewIndex).toBeUndefined(); - config.onViewOpened(view2); + config.viewOpened(view2); expect(config.openViewIndex).toBe(1); - config.onViewOpened(view1); + config.viewOpened(view1); expect(config.openViewIndex).toBe(0); }); }); diff --git a/test/exportPlotPoints.test.ts b/test/exportPlotPoints.test.ts index 1df7b4e..805121d 100644 --- a/test/exportPlotPoints.test.ts +++ b/test/exportPlotPoints.test.ts @@ -1,10 +1,11 @@ import { describe, expect, it } from "@jest/globals"; import { makeGameState } from "./fixture"; +import { asPlotPoints, runAsPlotPoints, type PlotPoint } from "../src/exports/plotPoints"; import { Game } from "../src/game"; import { HistoryManager } from "../src/history"; -import { asPlotPoints, type PlotPoint } from "../src/exports/plotPoints"; import { ConfigManager, type ViewConfig } from "../src/config"; +import type { LatestRun } from "../src/runTracking"; import type { universes } from "../src/enums"; function makeView(fields: Partial): ViewConfig { @@ -217,8 +218,8 @@ describe("Export", () => { }); expect(asPlotPoints(history.runs, history, config.views[0])).toEqual( [ - { run: 0, milestone: "Womlings arrival", day: 456, segment: 456 }, { run: 0, milestone: "Club", day: 123, dayDiff: 123, segment: 123 }, + { run: 0, milestone: "Womlings arrival", day: 456, segment: 456 }, { run: 0, milestone: "MAD", day: 789, dayDiff: 666, segment: 666 } ]); }); @@ -275,5 +276,164 @@ describe("Export", () => { { run: 0, milestone: resetName, day: 12, dayDiff: 12, segment: 12 }, ]); }); + + describe("Current run", () => { + it("should use the earliest unachieved enabled milestone as the next one", () => { + const game = new Game(makeGameState({})); + + const config = makeConfig(game, { + milestones: { + "tech:club": true, + "tech:wheel": true, + "tech:housing": true, + "tech:cottage": false, + "tech:metaphysics": true, + "reset:mad": true + } + }); + + const currentRun: LatestRun = { + run: 1, + universe: "standard", + resets: {}, + totalDays: 123, + milestones: { + "tech:club": 10, + "tech:housing": 20 + } + }; + + const milestones = ["tech:club", "tech:wheel", "tech:housing", "tech:cottage", "tech:metaphysics"]; + + expect(runAsPlotPoints(currentRun, config.views[0], 456, milestones)).toEqual( [ + { run: 456, milestone: "Club", day: 10, dayDiff: 10, segment: 10 }, + { run: 456, milestone: "Housing", day: 20, dayDiff: 10, segment: 10 }, + { run: 456, milestone: "Metaphysics", day: 123, dayDiff: 103, segment: 103, pending: true }, + ]); + }); + + it("should not use events when inferring the next milestone", () => { + const game = new Game(makeGameState({})); + + const config = makeConfig(game, { + milestones: { + "event:womlings": true, + "tech:club": true, + "reset:mad": true + } + }); + + const currentRun: LatestRun = { + run: 1, + universe: "standard", + resets: {}, + totalDays: 123, + milestones: { + } + }; + + const milestones = ["event:womlings", "tech:club"]; + + expect(runAsPlotPoints(currentRun, config.views[0], 456, milestones)).toEqual( [ + { run: 456, milestone: "Club", day: 123, dayDiff: 123, segment: 123, pending: true }, + ]); + }); + + it("should assume the next milestone is reset", () => { + const game = new Game(makeGameState({})); + + const config = makeConfig(game, { + milestones: { + "tech:club": true, + "tech:wheel": true, + "reset:mad": true + } + }); + + const currentRun: LatestRun = { + run: 1, + universe: "standard", + resets: {}, + totalDays: 123, + milestones: { + "tech:club": 10, + "tech:wheel": 20, + } + }; + + const milestones = ["tech:club", "tech:wheel"]; + + expect(runAsPlotPoints(currentRun, config.views[0], 456, milestones)).toEqual( [ + { run: 456, milestone: "Club", day: 10, dayDiff: 10, segment: 10 }, + { run: 456, milestone: "Wheel", day: 20, dayDiff: 10, segment: 10 }, + { run: 456, milestone: "MAD", day: 123, dayDiff: 103, segment: 103, pending: true }, + ]); + }); + + it("should not include extra milestones", () => { + const game = new Game(makeGameState({})); + + const config = makeConfig(game, { + milestones: { + "tech:club": true, + "tech:housing": true, + "reset:mad": true + } + }); + + const currentRun: LatestRun = { + run: 1, + universe: "standard", + resets: {}, + totalDays: 123, + milestones: { + "tech:club": 10, + "tech:wheel": 20, + "tech:housing": 30 + } + }; + + const milestones = ["tech:club", "tech:housing", "tech:metaphysics"]; + + expect(runAsPlotPoints(currentRun, config.views[0], 456, milestones)).toEqual( [ + { run: 456, milestone: "Club", day: 10, dayDiff: 10, segment: 10 }, + { run: 456, milestone: "Housing", day: 30, dayDiff: 20, segment: 20 }, + { run: 456, milestone: "MAD", day: 123, dayDiff: 93, segment: 93, pending: true }, + ]); + }); + + it("should not include disabled milestones", () => { + const game = new Game(makeGameState({})); + + const config = makeConfig(game, { + milestones: { + "tech:club": true, + "tech:wheel": false, + "tech:housing": true, + "reset:mad": true + } + }); + + const currentRun: LatestRun = { + run: 1, + universe: "standard", + resets: {}, + totalDays: 123, + milestones: { + "tech:club": 10, + "tech:wheel": 20, + "tech:housing": 30 + } + }; + + const milestones = ["tech:club", "tech:wheel", "tech:housing"]; + + expect(runAsPlotPoints(currentRun, config.views[0], 456, milestones)).toEqual( [ + { run: 456, milestone: "Club", day: 10, dayDiff: 10, segment: 10 }, + { run: 456, milestone: "Housing", day: 30, dayDiff: 20, segment: 10 }, + { run: 456, milestone: "MAD", day: 123, dayDiff: 93, segment: 93, pending: true }, + ]); + }); + }); }); });