diff --git a/src/app/archipelago-client.ts b/src/app/archipelago-client.ts index 9e05eae..5448b36 100644 --- a/src/app/archipelago-client.ts +++ b/src/app/archipelago-client.ts @@ -184,6 +184,32 @@ export async function initializeClient(initializeClientOptions: InitializeClient } })); + let prevRatHints = List(); + const itemNetworkIdToRatItem: Partial> = { }; + for (const [id, item] of defs.allItems.entries()) { + if (item.ratCount === 0) { + continue; + } + + itemNetworkIdToRatItem[itemNetworkNameLookup[itemName(item)]] = id; + } + const ratHints = computed(() => prevRatHints = prevRatHints.withMutations((rh) => { + const { team: myTeam, slot: mySlot } = client.players.self; + let seenCount = 0; + for (const hint of reactiveHints()) { + if (hint.item.id in itemNetworkIdToRatItem && hint.item.receiver.slot === mySlot && hint.item.receiver.team === myTeam) { + if (seenCount < rh.size) { + rh.set(seenCount, hint); + } + else { + rh.push(hint); + } + + ++seenCount; + } + } + })); + return { connectScreenState, client, @@ -192,6 +218,7 @@ export async function initializeClient(initializeClientOptions: InitializeClient slotData, hintedLocations, hintedItems, + ratHints, locationIsProgression, locationIsTrap, storedData, @@ -224,10 +251,12 @@ export type Message = function createReactiveHints(client: Client, destroyRef?: DestroyRef): Signal> { const hints = signal(List(client.items.hints)); function onHint(hint: Hint) { - hints.update(hints => hints.push(hint)); - } - function onHintUpdated(hint: Hint) { - hints.update(hints => hints.update(hints.findIndex(h => h.uniqueKey === hint.uniqueKey), hint, () => hint)); + hints.update((hints) => { + const hintIndex = hints.findIndex(h => h.uniqueKey === hint.uniqueKey); + return hintIndex < 0 + ? hints.push(hint) + : hints.set(hintIndex, hint); + }); } function onHints(newHints: readonly Hint[]) { hints.update(hints => hints.push(...newHints)); @@ -235,13 +264,13 @@ function createReactiveHints(client: Client, destroyRef?: DestroyRef): Signal
  • { client.items.off('hintsInitialized', onHints); client.items.off('hintReceived', onHint); client.items.off('hintFound', onHint); - client.items.off('hintUpdated', onHintUpdated); + client.items.off('hintUpdated', onHint); }); } return hints.asReadonly(); diff --git a/src/app/data/slot-data.ts b/src/app/data/slot-data.ts index 8626573..b7079b2 100644 --- a/src/app/data/slot-data.ts +++ b/src/app/data/slot-data.ts @@ -19,6 +19,7 @@ export interface AutopelagoClientAndData { messageLog: Signal>; hintedLocations: Signal>; hintedItems: Signal>; + ratHints: Signal>; slotData: AutopelagoSlotData; storedData: AutopelagoStoredData; storedDataKey: string; diff --git a/src/app/game-screen/game-content/game-tabs/game-tab-map/game-tab-map.ts b/src/app/game-screen/game-content/game-tabs/game-tab-map/game-tab-map.ts index debeaaf..c905e47 100644 --- a/src/app/game-screen/game-content/game-tabs/game-tab-map/game-tab-map.ts +++ b/src/app/game-screen/game-content/game-tabs/game-tab-map/game-tab-map.ts @@ -129,10 +129,17 @@ import { watchAnimations } from './watch-animations'; [cdkConnectedOverlayScrollStrategy]="tooltipScrollStrategy()"> @if (tooltipOrigin(); as origin) { @if (origin.location; as location) { - + } @else { - + } } @@ -468,6 +475,7 @@ interface LandmarkProps extends LocationProps { } interface CurrentTooltipOriginProps { + uid: symbol; location: number | null; element: HTMLElement; notifyDetached: () => void; diff --git a/src/app/game-screen/game-content/status-display/progression-item-status/progression-item-status.ts b/src/app/game-screen/game-content/status-display/progression-item-status/progression-item-status.ts index 3086db8..36a3778 100644 --- a/src/app/game-screen/game-content/status-display/progression-item-status/progression-item-status.ts +++ b/src/app/game-screen/game-content/status-display/progression-item-status/progression-item-status.ts @@ -27,9 +27,13 @@ import { RequestHint } from './request-hint'; appTooltip [tooltipContext]="tooltipContext" (tooltipOriginChange)="setTooltipOrigin($index, item.id, $event, true)"> + [style.--ap-object-position]="-item.offsetX() + 'px ' + -item.offsetY + 'px'"> + @if (item.id === 'rats') { + {{ ratCount() }} + } } @@ -39,26 +43,60 @@ import { RequestHint } from './request-hint'; [cdkConnectedOverlayOpen]="tooltipOrigin() !== null" [cdkConnectedOverlayUsePopover]="'inline'" (detach)="setTooltipOrigin(0, 0, null, false)"> - @let item = items()[tooltipOrigin()!.index]; -
    -

    {{item.name}}

    -
    “{{item.flavorText}}”
    - @if (hintForTooltipItem(); as hint) { -
    - At - {{ hint.item.locationName }} - in - {{ hint.item.sender }}'s world ({{ hintStatusText() }}). -
    - } -
    + @if (tooltipOrigin(); as origin) { + @let item = items()[origin.index]; +
    +

    {{item.name}}

    +
    “{{item.flavorText}}”
    + @if (item.id === 'rats') { + @if (ratHints().size > 0) { +
    + Hints: +
      + @for (hint of ratHints(); track $index) { +
    • + + {{ hint.item.name }} + + at + {{ hint.item.locationName }} + in + {{ hint.item.sender }}'s world ({{ statusText(hint) }}). +
    • + } +
    +
    + } + } + @else if (hintForTooltipItem(); as hint) { +
    + At + {{ hint.item.locationName }} + in + {{ hint.item.sender }}'s world ({{ statusText(hint) }}). +
    + } +
    + } `, styles: ` @@ -71,6 +109,7 @@ import { RequestHint } from './request-hint'; .item-container { margin: 5px; padding: 5px; + display: inline-grid; border: 2px solid black; border-radius: 8px; @@ -81,9 +120,21 @@ import { RequestHint } from './request-hint'; } .item { - object-fit: none; + grid-row: 1; + grid-column: 1; width: 64px; height: 64px; + &:not(.rats) { + object-fit: none; + object-position: var(--ap-object-position); + } + } + + .rat-count-corner { + grid-row: 1; + grid-column: 1; + align-self: end; + justify-self: end; } .box { @@ -98,6 +149,7 @@ import { RequestHint } from './request-hint'; gap: 10px; padding: 4px; background-color: theme.$region-color; + pointer-events: initial; .header { margin: 0; @@ -113,6 +165,10 @@ import { RequestHint } from './request-hint'; .hint { font-size: 8pt; } + + .rat-hint { + white-space: nowrap; + } } `, }) @@ -121,6 +177,7 @@ export class ProgressionItemStatus { readonly #dialog = inject(Dialog); readonly #gameStore = inject(GameStore); readonly #performanceInsensitiveAnimatableState = inject(PerformanceInsensitiveAnimatableState); + protected readonly ratCount = this.#performanceInsensitiveAnimatableState.ratCount.asReadonly(); protected readonly items: Signal; readonly #tooltipOrigin = signal(null); protected readonly tooltipOrigin = this.#tooltipOrigin.asReadonly(); @@ -128,10 +185,11 @@ export class ProgressionItemStatus { // without having to sit through the whole delay. protected readonly tooltipContext = createEmptyTooltipContext(); readonly #hintedItems = computed(() => this.#gameStore.game()?.hintedItems() ?? List(Repeat(null, BAKED_DEFINITIONS_FULL.allItems.length))); + protected readonly ratHints = computed(() => this.#gameStore.game()?.ratHints() ?? List()); protected readonly hintForTooltipItem = computed(() => { const item = this.tooltipOrigin()?.item ?? null; const hintedItems = [...this.#hintedItems()]; - return item === null + return item === null || item === 'rats' ? null : hintedItems[item] ?? null; }); @@ -141,27 +199,24 @@ export class ProgressionItemStatus { protected readonly HINT_STATUS_AVOID: Hint['status'] = 20; protected readonly HINT_STATUS_PRIORITY: Hint['status'] = 30; protected readonly HINT_STATUS_FOUND: Hint['status'] = 40; - protected readonly hintStatusText = computed(() => { - // https://github.com/ArchipelagoMW/Archipelago/blob/0.6.5/kvui.py#L1195-L1201 - switch (this.hintForTooltipItem()?.status) { - case this.HINT_STATUS_FOUND: return 'Found'; - case this.HINT_STATUS_UNSPECIFIED: return 'Unspecified'; - case this.HINT_STATUS_NO_PRIORITY: return 'No Priority'; - case this.HINT_STATUS_AVOID: return 'Avoid'; - case this.HINT_STATUS_PRIORITY: return 'Priority'; - default: return null; - } - }); constructor() { this.items = computed(() => { const victoryLocationYamlKey = this.#gameStore.victoryLocationYamlKey(); const lactoseIntolerant = this.#gameStore.lactoseIntolerant(); - return PROGRESSION_ITEMS_BY_VICTORY_LOCATION[victoryLocationYamlKey].map((itemYamlKey, index) => { + return [{ + index: 0, + id: 'rats', + collected: computed(() => this.ratCount() > 0), + name: 'Rats', + flavorText: null, + offsetX: computed(() => 0), + offsetY: BAKED_DEFINITIONS_FULL.progressionItemsByYamlKey.size * 65, + }, ...PROGRESSION_ITEMS_BY_VICTORY_LOCATION[victoryLocationYamlKey].map((itemYamlKey, index) => { const item = BAKED_DEFINITIONS_FULL.progressionItemsByYamlKey.get(itemYamlKey) ?? -1; const collected = computed(() => this.#performanceInsensitiveAnimatableState.receivedItemCountLookup()[item] > 0); return { - index, + index: index + 1, id: item, name: lactoseIntolerant ? BAKED_DEFINITIONS_FULL.allItems[item].lactoseIntolerantName @@ -171,11 +226,11 @@ export class ProgressionItemStatus { offsetX: computed(() => collected() ? 0 : 65), offsetY: index * 65, }; - }); + })]; }); } - protected setTooltipOrigin(index: number, item: number, props: TooltipOriginProps | null, fromDirective: boolean) { + protected setTooltipOrigin(index: number, item: number | 'rats', props: TooltipOriginProps | null, fromDirective: boolean) { this.#tooltipOrigin.update((prev) => { if (prev !== null && !fromDirective) { prev.notifyDetached(); @@ -220,11 +275,23 @@ export class ProgressionItemStatus { const { team, slot } = game.client.players.self; return player.team === team && player.slot === slot; } + + protected statusText(hint: Hint) { + // https://github.com/ArchipelagoMW/Archipelago/blob/0.6.5/kvui.py#L1195-L1201 + switch (hint.status) { + case this.HINT_STATUS_FOUND: return 'Found'; + case this.HINT_STATUS_UNSPECIFIED: return 'Unspecified'; + case this.HINT_STATUS_NO_PRIORITY: return 'No Priority'; + case this.HINT_STATUS_AVOID: return 'Avoid'; + case this.HINT_STATUS_PRIORITY: return 'Priority'; + default: return null; + } + } } interface ItemModel { index: number; - id: number; + id: number | 'rats'; name: string; flavorText: string | null; collected: Signal; @@ -233,8 +300,9 @@ interface ItemModel { } interface CurrentTooltipOriginProps { + uid: symbol; index: number; - item: number; + item: number | 'rats'; element: HTMLElement; notifyDetached: () => void; } diff --git a/src/app/tooltip-behavior.ts b/src/app/tooltip-behavior.ts index eb8fda1..0718e9d 100644 --- a/src/app/tooltip-behavior.ts +++ b/src/app/tooltip-behavior.ts @@ -1,5 +1,5 @@ import { CdkOverlayOrigin } from '@angular/cdk/overlay'; -import { Directive, ElementRef, inject, input, output } from '@angular/core'; +import { Directive, effect, ElementRef, inject, input, output } from '@angular/core'; @Directive({ selector: '[appTooltip]', @@ -14,6 +14,7 @@ import { Directive, ElementRef, inject, input, output } from '@angular/core'; ], }) export class TooltipBehavior { + readonly #uid = Symbol(); readonly #el = inject>(ElementRef); readonly tooltipContext = input(createEmptyTooltipContext()); @@ -21,6 +22,24 @@ export class TooltipBehavior { readonly delay = input(400); + constructor() { + const enter = () => { + this.onFocus(false); + }; + const leave = () => { + this.onBlur(false); + }; + effect((onCleanup) => { + const ctx = this.tooltipContext(); + ctx._notifyMouseEnterTooltipCallbacks.set(this.#uid, enter); + ctx._notifyMouseLeaveTooltipCallbacks.set(this.#uid, leave); + onCleanup(() => { + ctx._notifyMouseEnterTooltipCallbacks.delete(this.#uid); + ctx._notifyMouseLeaveTooltipCallbacks.delete(this.#uid); + }); + }); + } + protected onFocus(fromFocus: boolean) { const ctx = this.tooltipContext(); if (!fromFocus && ctx._tooltipIsOpenBecauseOfFocus) { @@ -108,6 +127,7 @@ export class TooltipBehavior { ctx._tooltipIsOpenBecauseOfFocus = fromFocus ? this : null; ctx._tooltipIsOpenBecauseOfMouse = !fromFocus; this.tooltipOriginChange.emit({ + uid: this.#uid, element: this.#el.nativeElement, notifyDetached: this.#detachTooltip, }); @@ -121,11 +141,21 @@ export class TooltipBehavior { } export function createEmptyTooltipContext(): TooltipContext { + const _notifyMouseEnterTooltipCallbacks = new Map void>(); + const _notifyMouseLeaveTooltipCallbacks = new Map void>(); return { _prevFocusTimeout: NaN, _prevBlurTimeout: NaN, _tooltipIsOpenBecauseOfFocus: null, _tooltipIsOpenBecauseOfMouse: false, + _notifyMouseEnterTooltipCallbacks, + _notifyMouseLeaveTooltipCallbacks, + notifyMouseEnterTooltip(uid: symbol) { + _notifyMouseEnterTooltipCallbacks.get(uid)?.(); + }, + notifyMouseLeaveTooltip(uid: symbol) { + _notifyMouseLeaveTooltipCallbacks.get(uid)?.(); + }, }; } @@ -134,9 +164,14 @@ export interface TooltipContext { _prevBlurTimeout: number; _tooltipIsOpenBecauseOfFocus: TooltipBehavior | null; _tooltipIsOpenBecauseOfMouse: boolean; + _notifyMouseEnterTooltipCallbacks: Map void>; + _notifyMouseLeaveTooltipCallbacks: Map void>; + notifyMouseEnterTooltip(uid: symbol): void; + notifyMouseLeaveTooltip(uid: symbol): void; } export interface TooltipOriginProps { + uid: symbol; element: HTMLElement; notifyDetached: () => void; }