Skip to content

Commit 8aca1e2

Browse files
authored
#138: Hint rats from sidebar (#144)
Allow hinting the "Rats" item group from the sidebar by putting in another synthetic item icon. Since this list can get very long, I've also done some stuff to the tooltips in that panel to let you interact with them.
1 parent 912d4e2 commit 8aca1e2

File tree

5 files changed

+191
-50
lines changed

5 files changed

+191
-50
lines changed

src/app/archipelago-client.ts

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,32 @@ export async function initializeClient(initializeClientOptions: InitializeClient
184184
}
185185
}));
186186

187+
let prevRatHints = List<Hint>();
188+
const itemNetworkIdToRatItem: Partial<Record<number, number>> = { };
189+
for (const [id, item] of defs.allItems.entries()) {
190+
if (item.ratCount === 0) {
191+
continue;
192+
}
193+
194+
itemNetworkIdToRatItem[itemNetworkNameLookup[itemName(item)]] = id;
195+
}
196+
const ratHints = computed(() => prevRatHints = prevRatHints.withMutations((rh) => {
197+
const { team: myTeam, slot: mySlot } = client.players.self;
198+
let seenCount = 0;
199+
for (const hint of reactiveHints()) {
200+
if (hint.item.id in itemNetworkIdToRatItem && hint.item.receiver.slot === mySlot && hint.item.receiver.team === myTeam) {
201+
if (seenCount < rh.size) {
202+
rh.set(seenCount, hint);
203+
}
204+
else {
205+
rh.push(hint);
206+
}
207+
208+
++seenCount;
209+
}
210+
}
211+
}));
212+
187213
return {
188214
connectScreenState,
189215
client,
@@ -192,6 +218,7 @@ export async function initializeClient(initializeClientOptions: InitializeClient
192218
slotData,
193219
hintedLocations,
194220
hintedItems,
221+
ratHints,
195222
locationIsProgression,
196223
locationIsTrap,
197224
storedData,
@@ -224,24 +251,26 @@ export type Message =
224251
function createReactiveHints(client: Client, destroyRef?: DestroyRef): Signal<List<Hint>> {
225252
const hints = signal(List<Hint>(client.items.hints));
226253
function onHint(hint: Hint) {
227-
hints.update(hints => hints.push(hint));
228-
}
229-
function onHintUpdated(hint: Hint) {
230-
hints.update(hints => hints.update(hints.findIndex(h => h.uniqueKey === hint.uniqueKey), hint, () => hint));
254+
hints.update((hints) => {
255+
const hintIndex = hints.findIndex(h => h.uniqueKey === hint.uniqueKey);
256+
return hintIndex < 0
257+
? hints.push(hint)
258+
: hints.set(hintIndex, hint);
259+
});
231260
}
232261
function onHints(newHints: readonly Hint[]) {
233262
hints.update(hints => hints.push(...newHints));
234263
}
235264
client.items.on('hintsInitialized', onHints);
236265
client.items.on('hintReceived', onHint);
237266
client.items.on('hintFound', onHint);
238-
client.items.on('hintUpdated', onHintUpdated);
267+
client.items.on('hintUpdated', onHint);
239268
if (destroyRef) {
240269
destroyRef.onDestroy(() => {
241270
client.items.off('hintsInitialized', onHints);
242271
client.items.off('hintReceived', onHint);
243272
client.items.off('hintFound', onHint);
244-
client.items.off('hintUpdated', onHintUpdated);
273+
client.items.off('hintUpdated', onHint);
245274
});
246275
}
247276
return hints.asReadonly();

src/app/data/slot-data.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export interface AutopelagoClientAndData {
1919
messageLog: Signal<List<Message>>;
2020
hintedLocations: Signal<List<Hint | null>>;
2121
hintedItems: Signal<List<Hint | null>>;
22+
ratHints: Signal<List<Hint>>;
2223
slotData: AutopelagoSlotData;
2324
storedData: AutopelagoStoredData;
2425
storedDataKey: string;

src/app/game-screen/game-content/game-tabs/game-tab-map/game-tab-map.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -129,10 +129,17 @@ import { watchAnimations } from './watch-animations';
129129
[cdkConnectedOverlayScrollStrategy]="tooltipScrollStrategy()">
130130
@if (tooltipOrigin(); as origin) {
131131
@if (origin.location; as location) {
132-
<app-location-tooltip [locationKey]="location" />
132+
<app-location-tooltip
133+
[locationKey]="location"
134+
(mouseenter)="tooltipContext.notifyMouseEnterTooltip(origin.uid)"
135+
(mouseleave)="tooltipContext.notifyMouseLeaveTooltip(origin.uid)"
136+
/>
133137
}
134138
@else {
135-
<app-player-tooltip />
139+
<app-player-tooltip
140+
(mouseenter)="tooltipContext.notifyMouseEnterTooltip(origin.uid)"
141+
(mouseleave)="tooltipContext.notifyMouseLeaveTooltip(origin.uid)"
142+
/>
136143
}
137144
}
138145
</ng-template>
@@ -468,6 +475,7 @@ interface LandmarkProps extends LocationProps {
468475
}
469476

470477
interface CurrentTooltipOriginProps {
478+
uid: symbol;
471479
location: number | null;
472480
element: HTMLElement;
473481
notifyDetached: () => void;

src/app/game-screen/game-content/status-display/progression-item-status/progression-item-status.ts

Lines changed: 109 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,13 @@ import { RequestHint } from './request-hint';
2727
appTooltip [tooltipContext]="tooltipContext" (tooltipOriginChange)="setTooltipOrigin($index, item.id, $event, true)">
2828
<!--suppress AngularNgOptimizedImage -->
2929
<img class="item"
30-
src="/assets/images/items.webp"
30+
[class.rats]="item.id === 'rats'"
31+
[src]="item.id === 'rats' ? '/assets/images/players/pack_rat.webp' : '/assets/images/items.webp'"
3132
[alt]="item.name"
32-
[style.object-position]="-item.offsetX() + 'px ' + -item.offsetY + 'px'">
33+
[style.--ap-object-position]="-item.offsetX() + 'px ' + -item.offsetY + 'px'">
34+
@if (item.id === 'rats') {
35+
<span class="rat-count-corner">{{ ratCount() }}</span>
36+
}
3337
</div>
3438
}
3539
</div>
@@ -39,26 +43,60 @@ import { RequestHint } from './request-hint';
3943
[cdkConnectedOverlayOpen]="tooltipOrigin() !== null"
4044
[cdkConnectedOverlayUsePopover]="'inline'"
4145
(detach)="setTooltipOrigin(0, 0, null, false)">
42-
@let item = items()[tooltipOrigin()!.index];
43-
<div class="tooltip">
44-
<h1 class="box header">{{item.name}}</h1>
45-
<div class="box flavor-text" [hidden]="!item.flavorText">“{{item.flavorText}}”</div>
46-
@if (hintForTooltipItem(); as hint) {
47-
<div class="box hint">
48-
At
49-
<span class="location-text">{{ hint.item.locationName }}</span>
50-
in
51-
<span class="player-text" [class.own-player-text]="isSelf(hint.item.sender)">{{ hint.item.sender }}</span>'s world (<span
52-
class="hint-text"
53-
[class.unspecified]="hint.status === HINT_STATUS_UNSPECIFIED"
54-
[class.no-priority]="hint.status === HINT_STATUS_NO_PRIORITY"
55-
[class.avoid]="hint.status === HINT_STATUS_AVOID"
56-
[class.priority]="hint.status === HINT_STATUS_PRIORITY"
57-
[class.found]="hint.status === HINT_STATUS_FOUND"
58-
>{{ hintStatusText() }}</span>).
59-
</div>
60-
}
61-
</div>
46+
@if (tooltipOrigin(); as origin) {
47+
@let item = items()[origin.index];
48+
<div
49+
class="tooltip"
50+
(mouseenter)="tooltipContext.notifyMouseEnterTooltip(origin.uid)"
51+
(mouseleave)="tooltipContext.notifyMouseLeaveTooltip(origin.uid)"
52+
>
53+
<h1 class="box header">{{item.name}}</h1>
54+
<div class="box flavor-text" [hidden]="!item.flavorText">“{{item.flavorText}}”</div>
55+
@if (item.id === 'rats') {
56+
@if (ratHints().size > 0) {
57+
<div class="box hint rat-hint">
58+
Hints:
59+
<ul>
60+
@for (hint of ratHints(); track $index) {
61+
<li>
62+
<span class="item-text" [class.progression]="hint.item.progression" [class.filler]="hint.item.filler"
63+
[class.useful]="hint.item.useful" [class.trap]="hint.item.trap">
64+
{{ hint.item.name }}
65+
</span>
66+
at
67+
<span class="location-text">{{ hint.item.locationName }}</span>
68+
in
69+
<span class="player-text" [class.own-player-text]="isSelf(hint.item.sender)">{{ hint.item.sender }}</span>'s world (<span
70+
class="hint-text"
71+
[class.unspecified]="hint.status === HINT_STATUS_UNSPECIFIED"
72+
[class.no-priority]="hint.status === HINT_STATUS_NO_PRIORITY"
73+
[class.avoid]="hint.status === HINT_STATUS_AVOID"
74+
[class.priority]="hint.status === HINT_STATUS_PRIORITY"
75+
[class.found]="hint.status === HINT_STATUS_FOUND"
76+
>{{ statusText(hint) }}</span>).
77+
</li>
78+
}
79+
</ul>
80+
</div>
81+
}
82+
}
83+
@else if (hintForTooltipItem(); as hint) {
84+
<div class="box hint">
85+
At
86+
<span class="location-text">{{ hint.item.locationName }}</span>
87+
in
88+
<span class="player-text" [class.own-player-text]="isSelf(hint.item.sender)">{{ hint.item.sender }}</span>'s world (<span
89+
class="hint-text"
90+
[class.unspecified]="hint.status === HINT_STATUS_UNSPECIFIED"
91+
[class.no-priority]="hint.status === HINT_STATUS_NO_PRIORITY"
92+
[class.avoid]="hint.status === HINT_STATUS_AVOID"
93+
[class.priority]="hint.status === HINT_STATUS_PRIORITY"
94+
[class.found]="hint.status === HINT_STATUS_FOUND"
95+
>{{ statusText(hint) }}</span>).
96+
</div>
97+
}
98+
</div>
99+
}
62100
</ng-template>
63101
`,
64102
styles: `
@@ -71,6 +109,7 @@ import { RequestHint } from './request-hint';
71109
.item-container {
72110
margin: 5px;
73111
padding: 5px;
112+
display: inline-grid;
74113
75114
border: 2px solid black;
76115
border-radius: 8px;
@@ -81,9 +120,21 @@ import { RequestHint } from './request-hint';
81120
}
82121
83122
.item {
84-
object-fit: none;
123+
grid-row: 1;
124+
grid-column: 1;
85125
width: 64px;
86126
height: 64px;
127+
&:not(.rats) {
128+
object-fit: none;
129+
object-position: var(--ap-object-position);
130+
}
131+
}
132+
133+
.rat-count-corner {
134+
grid-row: 1;
135+
grid-column: 1;
136+
align-self: end;
137+
justify-self: end;
87138
}
88139
89140
.box {
@@ -98,6 +149,7 @@ import { RequestHint } from './request-hint';
98149
gap: 10px;
99150
padding: 4px;
100151
background-color: theme.$region-color;
152+
pointer-events: initial;
101153
102154
.header {
103155
margin: 0;
@@ -113,6 +165,10 @@ import { RequestHint } from './request-hint';
113165
.hint {
114166
font-size: 8pt;
115167
}
168+
169+
.rat-hint {
170+
white-space: nowrap;
171+
}
116172
}
117173
`,
118174
})
@@ -121,17 +177,19 @@ export class ProgressionItemStatus {
121177
readonly #dialog = inject(Dialog);
122178
readonly #gameStore = inject(GameStore);
123179
readonly #performanceInsensitiveAnimatableState = inject(PerformanceInsensitiveAnimatableState);
180+
protected readonly ratCount = this.#performanceInsensitiveAnimatableState.ratCount.asReadonly();
124181
protected readonly items: Signal<readonly ItemModel[]>;
125182
readonly #tooltipOrigin = signal<CurrentTooltipOriginProps | null>(null);
126183
protected readonly tooltipOrigin = this.#tooltipOrigin.asReadonly();
127184
// all tooltips here should use the same context, so that the user can quickly switch between them
128185
// without having to sit through the whole delay.
129186
protected readonly tooltipContext = createEmptyTooltipContext();
130187
readonly #hintedItems = computed(() => this.#gameStore.game()?.hintedItems() ?? List(Repeat(null, BAKED_DEFINITIONS_FULL.allItems.length)));
188+
protected readonly ratHints = computed(() => this.#gameStore.game()?.ratHints() ?? List<Hint>());
131189
protected readonly hintForTooltipItem = computed(() => {
132190
const item = this.tooltipOrigin()?.item ?? null;
133191
const hintedItems = [...this.#hintedItems()];
134-
return item === null
192+
return item === null || item === 'rats'
135193
? null
136194
: hintedItems[item] ?? null;
137195
});
@@ -141,27 +199,24 @@ export class ProgressionItemStatus {
141199
protected readonly HINT_STATUS_AVOID: Hint['status'] = 20;
142200
protected readonly HINT_STATUS_PRIORITY: Hint['status'] = 30;
143201
protected readonly HINT_STATUS_FOUND: Hint['status'] = 40;
144-
protected readonly hintStatusText = computed(() => {
145-
// https://github.com/ArchipelagoMW/Archipelago/blob/0.6.5/kvui.py#L1195-L1201
146-
switch (this.hintForTooltipItem()?.status) {
147-
case this.HINT_STATUS_FOUND: return 'Found';
148-
case this.HINT_STATUS_UNSPECIFIED: return 'Unspecified';
149-
case this.HINT_STATUS_NO_PRIORITY: return 'No Priority';
150-
case this.HINT_STATUS_AVOID: return 'Avoid';
151-
case this.HINT_STATUS_PRIORITY: return 'Priority';
152-
default: return null;
153-
}
154-
});
155202

156203
constructor() {
157204
this.items = computed(() => {
158205
const victoryLocationYamlKey = this.#gameStore.victoryLocationYamlKey();
159206
const lactoseIntolerant = this.#gameStore.lactoseIntolerant();
160-
return PROGRESSION_ITEMS_BY_VICTORY_LOCATION[victoryLocationYamlKey].map((itemYamlKey, index) => {
207+
return [{
208+
index: 0,
209+
id: 'rats',
210+
collected: computed(() => this.ratCount() > 0),
211+
name: 'Rats',
212+
flavorText: null,
213+
offsetX: computed(() => 0),
214+
offsetY: BAKED_DEFINITIONS_FULL.progressionItemsByYamlKey.size * 65,
215+
}, ...PROGRESSION_ITEMS_BY_VICTORY_LOCATION[victoryLocationYamlKey].map((itemYamlKey, index) => {
161216
const item = BAKED_DEFINITIONS_FULL.progressionItemsByYamlKey.get(itemYamlKey) ?? -1;
162217
const collected = computed(() => this.#performanceInsensitiveAnimatableState.receivedItemCountLookup()[item] > 0);
163218
return {
164-
index,
219+
index: index + 1,
165220
id: item,
166221
name: lactoseIntolerant
167222
? BAKED_DEFINITIONS_FULL.allItems[item].lactoseIntolerantName
@@ -171,11 +226,11 @@ export class ProgressionItemStatus {
171226
offsetX: computed(() => collected() ? 0 : 65),
172227
offsetY: index * 65,
173228
};
174-
});
229+
})];
175230
});
176231
}
177232

178-
protected setTooltipOrigin(index: number, item: number, props: TooltipOriginProps | null, fromDirective: boolean) {
233+
protected setTooltipOrigin(index: number, item: number | 'rats', props: TooltipOriginProps | null, fromDirective: boolean) {
179234
this.#tooltipOrigin.update((prev) => {
180235
if (prev !== null && !fromDirective) {
181236
prev.notifyDetached();
@@ -220,11 +275,23 @@ export class ProgressionItemStatus {
220275
const { team, slot } = game.client.players.self;
221276
return player.team === team && player.slot === slot;
222277
}
278+
279+
protected statusText(hint: Hint) {
280+
// https://github.com/ArchipelagoMW/Archipelago/blob/0.6.5/kvui.py#L1195-L1201
281+
switch (hint.status) {
282+
case this.HINT_STATUS_FOUND: return 'Found';
283+
case this.HINT_STATUS_UNSPECIFIED: return 'Unspecified';
284+
case this.HINT_STATUS_NO_PRIORITY: return 'No Priority';
285+
case this.HINT_STATUS_AVOID: return 'Avoid';
286+
case this.HINT_STATUS_PRIORITY: return 'Priority';
287+
default: return null;
288+
}
289+
}
223290
}
224291

225292
interface ItemModel {
226293
index: number;
227-
id: number;
294+
id: number | 'rats';
228295
name: string;
229296
flavorText: string | null;
230297
collected: Signal<boolean>;
@@ -233,8 +300,9 @@ interface ItemModel {
233300
}
234301

235302
interface CurrentTooltipOriginProps {
303+
uid: symbol;
236304
index: number;
237-
item: number;
305+
item: number | 'rats';
238306
element: HTMLElement;
239307
notifyDetached: () => void;
240308
}

0 commit comments

Comments
 (0)