Some attempts at supporting some kind of livesplit functionality.

main
isark 1 year ago
parent 3272c3bda2
commit cd042d99ca

1
.gitignore vendored

@ -45,6 +45,7 @@ Thumbs.db
package-lock.json
processed_world_areas.json
processed_world_areas_full.json
releases
.env
releaser_key

File diff suppressed because one or more lines are too long

@ -50,6 +50,9 @@ pub struct Config {
#[serde(default = "Config::default_enable_stopwatch")]
pub enable_stopwatch: bool,
#[serde(default = "Config::default_run_compare_history")]
pub run_compare_history: Option<String>,
}
impl Default for Config {
@ -70,6 +73,7 @@ impl Default for Config {
num_visible: Self::default_plan_num_visible(),
offset: Self::default_plan_offset(),
enable_stopwatch: Self::default_enable_stopwatch(),
run_compare_history: Self::default_run_compare_history(),
}
}
}
@ -125,4 +129,8 @@ impl Config {
fn default_enable_stopwatch() -> bool {
false
}
fn default_run_compare_history() -> Option<String> {
None
}
}

@ -88,6 +88,13 @@ pub struct PlanElement {
edited: bool,
anchor_act: Option<u8>,
#[serde(default, skip_serializing_if = "is_false")]
checkpoint: bool,
}
fn is_false(flag: &bool) -> bool {
!flag
}
impl PlanElement {
@ -144,6 +151,7 @@ pub fn convert_old(path: PathBuf) -> Option<Plan> {
uuid: PlanElement::generate_uuid(),
edited: PlanElement::edited(),
anchor_act: None,
checkpoint: false,
})
.collect::<Vec<PlanElement>>(),
metadata: PlanMetadata {

@ -30,7 +30,6 @@ export class Plan {
identifier?: string;
last_stored_time?: string;
private path?: string;
private selfSaveSubject: Subject<void> = new Subject<void>();
@ -53,6 +52,10 @@ export class Plan {
this.path = path;
}
isNext(zoneId: string, current = this.current) {
return current + 1 < this.plan.length && zoneId === this.plan[current + 1].area_key;
}
next() {
if (this.current + 1 < this.plan!.length) {
this.current++;
@ -86,7 +89,7 @@ export class Plan {
}
private directSelfSave() {
invoke('save_plan_at_path', {path: this.path, plan: this.toInterface()});
invoke('save_plan_at_path', { path: this.path, plan: this.toInterface() });
}
}
@ -97,4 +100,7 @@ export interface PlanElement {
uuid?: string;
edited: boolean;
anchor_act?: number;
checkpoint?: boolean;
checkpoint_millis?: number;
checkpoint_your_millis?: number;
}

@ -0,0 +1,139 @@
import { Injectable } from '@angular/core';
import { EntryType, RunHistory } from './time-tracker.service';
import { Plan } from '../_models/plan';
export interface RunStat {
zoneName: string;
entryTime: string;
estimatedExit: string;
estimatedTimeSpent: string;
}
export interface AggregateRunStat {
zoneName: string;
aggregateFirstEntry: string;
aggregateLastExit: string;
aggregateTimeSpent: string;
aggregateNumEntries: string;
}
export interface UnformattedAggregateRunStat {
zoneId: string;
aggregateFirstEntry: number;
aggregateLastExit: number;
aggregateTimeSpent: number;
aggregateNumEntries: number;
}
export interface UnformattedAggregationData {
aggregation: UnformattedAggregateRunStat[];
aggregateNAId: string;
}
export interface UnformattedRunStat {
zoneId: string;
entryTime: number;
estimatedExit?: number;
estimatedTimeSpent?: number;
entryType: EntryType;
}
export type RunStatType = RunStat | AggregateRunStat;
@Injectable({
providedIn: 'root'
})
export class RunStatService {
/// practically which zone can't have a last exit time as last exit is not determinable for the last entry
aggregateNAId?: string;
constructor() { }
calcAggregated(data: RunHistory): UnformattedAggregationData {
const aggregation = new Map<string, UnformattedAggregateRunStat>();
this.aggregateNAId = data.entries[data.entries.length - 1].zone;
data.entries.forEach((entry, index) => {
const hasExit = !(data.entries.length - 1 === index);
let aggregate: UnformattedAggregateRunStat = {
zoneId: entry.zone,
aggregateFirstEntry: entry.current_elapsed_millis,
aggregateLastExit: hasExit ? data.entries[index + 1].current_elapsed_millis : 0,
aggregateTimeSpent: hasExit ? (data.entries[index + 1].current_elapsed_millis - data.entries[index].current_elapsed_millis) : 0,
aggregateNumEntries: 1,
}
const existing = aggregation.get(entry.zone);
if (existing) {
existing.aggregateLastExit = aggregate.aggregateLastExit;
existing.aggregateTimeSpent += aggregate.aggregateTimeSpent;
existing.aggregateNumEntries++;
}
aggregation.set(entry.zone, existing ?? aggregate);
});
return {
aggregation: Array.from(aggregation.values()),
aggregateNAId: this.aggregateNAId
};
}
calcDirect(data: RunHistory): UnformattedRunStat[] {
return data.entries.map((entry, index) => {
const hasExit = !(data.entries.length - 1 === index);
return {
zoneId: entry.zone,
entryTime: entry.current_elapsed_millis,
estimatedExit: hasExit ? data.entries[index + 1].current_elapsed_millis : undefined,
estimatedTimeSpent: hasExit ? (data.entries[index + 1].current_elapsed_millis - data.entries[index].current_elapsed_millis) : undefined,
entryType: entry.type,
}
})
}
insertTimesAtCheckpoints(history: RunHistory, plan: Plan) {
const data = this.calcDirect(history);
let fakeCurrent = 0;
console.log("history", history);
data.forEach(entry => {
switch (entry.entryType) {
case EntryType.PlanForceNext:
fakeCurrent++;
break;
case EntryType.PlanForcePrev:
fakeCurrent--;
break;
case EntryType.ZoneEnter:
if (plan.isNext(entry.zoneId, fakeCurrent)) {
fakeCurrent++;
if (plan.plan[fakeCurrent].checkpoint) {
plan.plan[fakeCurrent].checkpoint_millis = entry.entryTime;
}
}
break;
}
});
if (fakeCurrent < plan.plan.length - 1) {
for (let current = fakeCurrent; current < plan.plan.length; current++) {
if (plan.plan[current].checkpoint) {
plan.plan[current].checkpoint_millis = -1;
}
}
}
console.log("Inserted checkpoint times", plan);
}
}

@ -130,6 +130,8 @@ export class TimeTrackerService {
this.askResume(plan);
});
} else {
this.currentRunHistory = this.createNew(plan.name);
}
}

@ -41,13 +41,13 @@
</div>
<div class="row">
<mat-slide-toggle class="col-5" color="accent" [(ngModel)]="autoScrollToEnd">
Auto scroll to latest
</mat-slide-toggle>
<mat-slide-toggle class="col-5" color="accent" [(ngModel)]="autoScrollToEnd">
Auto scroll to latest
</mat-slide-toggle>
<mat-slide-toggle class="col-4" color="accent" [(ngModel)]="reverseDisplay">
Reverse display
</mat-slide-toggle>
<mat-slide-toggle class="col-4" color="accent" [(ngModel)]="reverseDisplay">
Reverse display
</mat-slide-toggle>
<div class="col-3 d-flex justify-content-end ">
<button class="" mat-stroked-button color="warn" (click)="clearPlan()">Clear</button>
</div>
@ -74,12 +74,19 @@
<h2>Plan</h2>
<div cdkDropList #planList [cdkDropListData]="filterPlanElements()" class="list"
(cdkDropListDropped)="dropHandler($event)" [cdkDropListDisabled]="disabledPlanDD"
[cdkDropListEnterPredicate]="canDrop"
[cdkDropListSortPredicate]="sortPredicate.bind(this)">
<div class="box" *ngFor="let item of filterPlanElements(); index as boxIndex" cdkDrag [cdkDragDisabled]="(!!this.planFilterAct.value) && boxIndex == 0" (contextmenu)="addNote($event, item)">
[cdkDropListEnterPredicate]="canDrop" [cdkDropListSortPredicate]="sortPredicate.bind(this)">
<div class="box" *ngFor="let item of filterPlanElements(); index as boxIndex" cdkDrag
[cdkDragDisabled]="(!!this.planFilterAct.value) && boxIndex == 0" (contextmenu)="addNote($event, item)">
<div class="content">
<div class="zone-name">{{areasMap?.get(item.area_key)?.name}}</div>
<div class="act">Act {{areasMap?.get(item.area_key)?.act}}</div>
<div class="content-left">
<div class="zone-name">{{areasMap?.get(item.area_key)?.name}}</div>
<div class="act">Act {{areasMap?.get(item.area_key)?.act}}
<mat-slide-toggle class="" color="accent" [(ngModel)]="item.checkpoint">
Checkpoint zone
</mat-slide-toggle>
</div>
</div>
</div>
<div *ngIf="item.notes" class="notes">(Note)</div>
<div class="index">#{{planIndexOf(item)}}</div>

@ -13,10 +13,14 @@
<ng-template let-slide let-index="index">
<div class="zone-slide" [style.color]="configService.config.noteDefaultFg"
[style.border]="index == currentPlan.current ? '1px white solid' : 'none'">
{{worldAreaMap!.get(slide.area_key)!.name}}
<div *ngIf="showDiff(slide)">
<div [class]="yourDiffClass(slide)">{{yourDiff(slide)}}</div>
</div>
<div>{{worldAreaMap!.get(slide.area_key)!.name}}</div>
<div class="text-marker d-flex flex-row">
<div>{{cpMillis(slide)}}</div>
<div class="waypoint-text" *ngIf="hasWaypoint(slide.area_key)">(W)</div>
<div class="trial-text" *ngIf="hasTrial(slide.area_key)">(T)</div>
</div>

@ -204,4 +204,11 @@ notes {
position: absolute;
bottom: 0;
right: 0;
}
.negative-diff {
color: green;
}
.positive-diff {
color: red;
}

@ -9,15 +9,15 @@ import { PlanService, UrlError } from '../_services/plan.service';
import { Plan, PlanElement, PlanMetadata } from '../_models/plan';
import { WorldAreaService } from '../_services/world-area.service';
import { WorldArea } from '../_models/world-area';
import { Subscription, from, timer } from 'rxjs';
import { Subscription, from } from 'rxjs';
import { open } from '@tauri-apps/api/dialog';
import { OverlayService, StateEvent } from '../_services/overlay.service';
import { appWindow } from '@tauri-apps/api/window';
import { EventsService } from '../_services/events.service';
import { Event } from '@tauri-apps/api/event';
import { MatDialog } from '@angular/material/dialog';
import { ResumeDialog } from './resume-dialog.component';
import { TimeTrackerService } from '../_services/time-tracker.service';
import { RunStatService } from '../_services/run-stat.service';
enum Resume {
Discard,
@ -56,7 +56,20 @@ export class PlanDisplayComponent implements AfterViewInit, OnInit {
recentUpdateAttempts: Map<string, Date | UrlError> = new Map<string, Date | UrlError>();
constructor(private events: EventsService, public configService: ConfigService, private cdr: ChangeDetectorRef, private shortcut: ShortcutService, public planService: PlanService, public worldAreaService: WorldAreaService, public overlayService: OverlayService, private zone: NgZone, public dialog: MatDialog, public timeTrackerService: TimeTrackerService) {
constructor(
public configService: ConfigService,
public planService: PlanService,
public worldAreaService: WorldAreaService,
public overlayService: OverlayService,
public dialog: MatDialog,
public timeTrackerService: TimeTrackerService,
private events: EventsService,
private cdr: ChangeDetectorRef,
private shortcut: ShortcutService,
private zone: NgZone,
private runStatService: RunStatService,
) {
window.addEventListener("resize", () => {
this.zone.run(() => {
this.windowInitHandler()
@ -64,14 +77,17 @@ export class PlanDisplayComponent implements AfterViewInit, OnInit {
});
this.planService.getStoredPlans().subscribe(plans => {
console.log("got new stored plans");
this.previousPlans = plans;
})
this.planService.getCurrentPlan().subscribe(plan => {
this.currentPlan = plan;
if (this.configService.config.enableStopwatch) {
this.loadComparisonData(this.currentPlan);
}
this.timeTrackerService.onNewRun(plan);
//Close settings anytime we get a new current plan.
this.settingsOpen = false;
@ -81,20 +97,26 @@ export class PlanDisplayComponent implements AfterViewInit, OnInit {
this.registerOnZoneEnter();
}
loadComparisonData(plan: Plan) {
if (!this.configService.config.runCompareHistory) {
return;
}
this.timeTrackerService.loadHistory(this.configService.config.runCompareHistory).subscribe(history => {
if (history) {
this.runStatService.insertTimesAtCheckpoints(history, plan);
}
});
}
get disablePlans(): boolean {
return this.checkingPlanUpdate;
}
registerOnZoneEnter() {
appWindow.listen("entered", (entered) => {
if (this.currentPlan) {
const current = this.currentPlan.current;
const length = this.currentPlan.plan.length;
if (current + 1 < length) {
if (entered.payload === this.currentPlan.plan[current + 1].area_key) {
this.zone.run(() => this.next());
}
}
if (this.currentPlan && typeof entered.payload == "string" && this.currentPlan.isNext(entered.payload)) {
this.zone.run(() => this.next());
}
});
}
@ -211,7 +233,7 @@ export class PlanDisplayComponent implements AfterViewInit, OnInit {
setupBinds() {
if (this.currentSlides && !this.bindsAreSetup) {
this.nextBind = this.shortcut.register(this.configService.config.prev).subscribe((_shortcut) => {
this.prev();
if (this.configService.config.enableStopwatch) {
@ -225,7 +247,7 @@ export class PlanDisplayComponent implements AfterViewInit, OnInit {
this.timeTrackerService.onForceNext(this.currentPlan!.plan[this.currentPlan!.current].area_key);
}
});
this.bindsAreSetup = true;
}
}
@ -240,17 +262,55 @@ export class PlanDisplayComponent implements AfterViewInit, OnInit {
next() {
if (this.overlayService.visible) {
this.currentPlan!.next();
this.checkCheckpoint();
this.currentSlides?.next();
this.zoneSlides?.next();
}
}
checkCheckpoint() {
if(!this.currentPlan || !this.timeTrackerService.isActive) return;
const currentElem = this.currentPlan.plan[this.currentPlan.current];
if(currentElem.checkpoint && !currentElem.checkpoint_your_millis) {
currentElem.checkpoint_your_millis = this.timeTrackerService.elapsedTimeMillis;
}
}
yourDiff(element: PlanElement) {
if (!element.checkpoint || !element.checkpoint_your_millis || !element.checkpoint_millis) return "";
const diff = element.checkpoint_your_millis - element.checkpoint_millis;
const neg = diff < 0;
const abs = Math.abs(diff);
const cssClass = neg ? "negative-diff" : "positive-diff";
return `${neg ? "-" : "+"}${this.timeTrackerService.hmsTimestamp(abs)}`;
}
yourDiffClass(element: PlanElement): string {
if (!element.checkpoint || !element.checkpoint_your_millis || !element.checkpoint_millis) return "";
const diff = element.checkpoint_your_millis - element.checkpoint_millis;
const neg = diff < 0;
return neg ? "negative-diff" : "positive-diff";
}
showDiff(element: PlanElement) {
return element.checkpoint && element.checkpoint_your_millis && element.checkpoint_millis;
}
cpMillis(element: PlanElement) {
if(!element.checkpoint) return "";
if(!element.checkpoint_millis) return "N/A";
return this.timeTrackerService.hmsTimestamp(element.checkpoint_millis);
}
prev() {
if (this.overlayService.visible) {
this.currentPlan!.prev();
this.currentSlides?.prev();
this.zoneSlides?.prev();
}
}

@ -10,6 +10,7 @@
</div>
<div class="d-flex flex-column justify-content-center align-items-center">
<button mat-stroked-button color="accent" (click)="loadMain(item.key)">Load</button>
<button mat-stroked-button color="accent" (click)="setComparison(item.key)">Live compare</button>
</div>
</div>
</div>

@ -1,6 +1,6 @@
import { Component, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RunHistory, RunHistoryMetadata, TimeTrackerService } from '../_services/time-tracker.service';
import { EntryType, RunHistory, RunHistoryMetadata, TimeTrackerService } from '../_services/time-tracker.service';
import { MatTableModule } from '@angular/material/table';
import { MatSlideToggleModule } from '@angular/material/slide-toggle';
import { FormsModule } from '@angular/forms';
@ -8,34 +8,10 @@ import { ScrollingModule } from '@angular/cdk/scrolling';
import { MatButtonModule } from '@angular/material/button';
import { WorldAreaService } from '../_services/world-area.service';
import { WorldArea } from '../_models/world-area';
import { AggregateRunStat, RunStat, RunStatService, RunStatType, UnformattedAggregationData, UnformattedRunStat } from '../_services/run-stat.service';
import { ConfigService } from '../_services/config.service';
export interface RunStat {
zoneName: string;
entryTime: string;
estimatedExit: string;
estimatedTimeSpent: string;
}
export interface AggregateRunStat {
zoneName: string;
aggregateFirstEntry: string;
aggregateLastExit: string;
aggregateTimeSpent: string;
aggregateNumEntries: string;
}
interface UnformattedAggregateRunStat {
zoneId: string;
aggregateFirstEntry: number;
aggregateLastExit: number;
aggregateTimeSpent: number;
aggregateNumEntries: number;
}
type RunStatType = RunStat | AggregateRunStat;
@Component({
selector: 'app-run-stats',
@ -46,16 +22,18 @@ type RunStatType = RunStat | AggregateRunStat;
})
export class RunStatsComponent implements OnInit {
aggregated?: AggregateRunStat[];
/// practically which zone can't have a last exit time as last exit is not determinable for the last entry
aggregateNAId?: string;
direct?: RunStat[];
aggregate: boolean = true;
cache?: Map<string, RunHistoryMetadata>;
worldAreaMap?: Map<string, WorldArea>;
constructor(private timeTrackerService: TimeTrackerService, private worldAreaService: WorldAreaService) {
constructor(
private timeTrackerService: TimeTrackerService,
private worldAreaService: WorldAreaService,
private runStatService: RunStatService,
private configService: ConfigService
) {
this.worldAreaService.getFullWorldAreas().subscribe((data) => {
this.worldAreaMap = data;
})
@ -87,44 +65,12 @@ export class RunStatsComponent implements OnInit {
}));
}
dateFormat(value: number) {
return new Date(value).toLocaleString();
}
onLoad(data: RunHistory) {
this.direct = this.calcDirect(data);
this.aggregated = this.calcAggregated(data);
}
calcAggregated(data: RunHistory): AggregateRunStat[] {
const aggregation = new Map<string, UnformattedAggregateRunStat>();
this.aggregateNAId = data.entries[data.entries.length - 1].zone;
data.entries.forEach((entry, index) => {
const hasExit = !(data.entries.length - 1 === index);
formatAggregate(data: UnformattedAggregationData) {
const { aggregation, aggregateNAId } = data;
let aggregate: UnformattedAggregateRunStat = {
zoneId: entry.zone,
aggregateFirstEntry: entry.current_elapsed_millis,
aggregateLastExit: hasExit ? data.entries[index + 1].current_elapsed_millis : 0,
aggregateTimeSpent: hasExit ? (data.entries[index + 1].current_elapsed_millis - data.entries[index].current_elapsed_millis) : 0,
aggregateNumEntries: 1,
}
const existing = aggregation.get(entry.zone);
if (existing) {
existing.aggregateLastExit = aggregate.aggregateLastExit;
existing.aggregateTimeSpent += aggregate.aggregateTimeSpent;
existing.aggregateNumEntries++;
}
aggregation.set(entry.zone, existing ?? aggregate);
});
return Array.from(aggregation.values()).map((entry) => {
return aggregation.map((entry) => {
let aggregateTimeSpent;
if (this.aggregateNAId === entry.zoneId) {
if (aggregateNAId === entry.zoneId) {
aggregateTimeSpent = this.timeTrackerService.hmsTimestamp(entry.aggregateTimeSpent) + " + N/A"
} else {
aggregateTimeSpent = this.timeTrackerService.hmsTimestamp(entry.aggregateTimeSpent);
@ -133,26 +79,33 @@ export class RunStatsComponent implements OnInit {
return {
zoneName: this.resolveZone(entry.zoneId),
aggregateFirstEntry: this.timeTrackerService.hmsTimestamp(entry.aggregateFirstEntry),
aggregateLastExit: this.aggregateNAId === entry.zoneId ? "N/A" : this.timeTrackerService.hmsTimestamp(entry.aggregateLastExit),
aggregateLastExit: aggregateNAId === entry.zoneId ? "N/A" : this.timeTrackerService.hmsTimestamp(entry.aggregateLastExit),
aggregateTimeSpent: aggregateTimeSpent,
aggregateNumEntries: entry.aggregateNumEntries.toString(),
}
});
}
calcDirect(data: RunHistory): RunStat[] {
return data.entries.map((entry, index) => {
const hasExit = !(data.entries.length - 1 === index);
formatDirect(direct: UnformattedRunStat[]) {
return direct.filter(entry => entry.entryType == EntryType.ZoneEnter).map(entry => {
return {
zoneName: this.resolveZone(entry.zone),
entryTime: this.timeTrackerService.hmsTimestamp(entry.current_elapsed_millis),
estimatedExit: hasExit ? this.timeTrackerService.hmsTimestamp(data.entries[index + 1].current_elapsed_millis) : "N/A",
estimatedTimeSpent: hasExit ? this.timeTrackerService.hmsTimestamp(data.entries[index + 1].current_elapsed_millis - data.entries[index].current_elapsed_millis) : "N/A",
zoneName: this.resolveZone(entry.zoneId),
entryTime: this.timeTrackerService.hmsTimestamp(entry.entryTime),
estimatedExit: entry.estimatedExit ? this.timeTrackerService.hmsTimestamp(entry.estimatedExit) : "N/A",
estimatedTimeSpent: entry.estimatedTimeSpent ? this.timeTrackerService.hmsTimestamp(entry.estimatedTimeSpent) : "N/A",
}
})
}
dateFormat(value: number) {
return new Date(value).toLocaleString();
}
onLoad(data: RunHistory) {
this.direct = this.formatDirect(this.runStatService.calcDirect(data));
this.aggregated = this.formatAggregate(this.runStatService.calcAggregated(data));
}
hms(time: number) {
return this.timeTrackerService.hmsTimestamp(time);
}
@ -170,6 +123,11 @@ export class RunStatsComponent implements OnInit {
reset() {
this.aggregated = undefined;
this.direct = undefined;
}
setComparison(id: string) {
this.configService.config.runCompareHistory = id;
}
get displayedColumns() {

Loading…
Cancel
Save