Two teams try and fill in any horizontal, vertical, or diagonal line on a bingo board by playing maps on osu!
osu.bingo
osu
1<script lang="ts">
2 import EventScore from './EventScore.svelte';
3 import { fly } from 'svelte/transition';
4 import EventTime from './EventTime.svelte';
5 import EventStart from './EventStart.svelte';
6 import { afterUpdate } from 'svelte';
7 import { game as store, game_rules } from '$lib/stores';
8 import { getEvents } from '$lib/gamerules/get_rules';
9 import type { Options } from '$lib/gamerules/options';
10
11 type Event = ScoreInfo | GameEvent | StartEvent;
12 type StartEvent = {
13 type: 'start';
14 date: Date;
15 };
16 const isStart = (e: Event): e is StartEvent => e.type == 'start';
17
18 type GameEvent = {
19 id: string;
20 type: 'time';
21 date: Date;
22 data: Options.Event;
23 };
24 const isGameEvent = (e: Event): e is GameEvent => e.type == 'time';
25
26 type ScoreInfo = {
27 type: 'score';
28 date: Date;
29 data: {
30 score: Bingo.Card.FullScore;
31 square: Bingo.Card.FullSquare;
32 square_index: number;
33 };
34 };
35 const isScore = (e: Event): e is ScoreInfo => e.type == 'score';
36
37 let events: Event[] = [];
38 store.subscribe((card) => {
39 if (!card || !card.squares) return;
40
41 // Sort stuff by date to add to list
42 const scores: {
43 score: Bingo.Card.FullScore;
44 square: Bingo.Card.FullSquare;
45 square_index: number;
46 }[] = [];
47 const time_events: Options.Event[] = [];
48 for (let i = 0; i < card.squares.length; i++) {
49 const square = card.squares[i];
50 for (const score of square.scores) {
51 scores.push({
52 score,
53 square,
54 square_index: i
55 });
56 }
57 }
58
59 const eventList = getEvents(card) ?? [];
60 for (const event of eventList) {
61 time_events.push(event);
62 }
63
64 if (card.start_time != null && card.state != 0) {
65 const startEvent = events.find(isStart);
66 if (!startEvent) {
67 events.push({
68 type: 'start',
69 date: card.start_time
70 });
71 }
72 }
73
74 // Add new scores
75 const score_id_map = events.filter((x) => isScore(x)).map((x) => x.data.score.id);
76 for (const score of scores) {
77 if (!score_id_map.includes(score.score.id)) {
78 events.push({
79 type: 'score',
80 date: score.score.date,
81 data: score
82 });
83 }
84 }
85
86 const event_id_map = events.filter((x) => isGameEvent(x)).map((x) => x.id);
87 for (const event of time_events) {
88 const id = event.event + ((card.start_time?.valueOf() ?? 0) + event.seconds_after_start);
89 if (!event_id_map.includes(id)) {
90 events.push({
91 id,
92 type: 'time',
93 date: new Date((card.start_time?.valueOf() ?? 0) + event.seconds_after_start * 1000),
94 data: event
95 });
96 }
97 }
98
99 events.sort((a, b) => new Date(a.date).valueOf() - new Date(b.date).valueOf());
100
101 // Because we haven't upgraded to svelte 5 yet, we have to do this to tell svelte to update the list
102 events = [...events];
103 });
104
105 // Autoscroll
106 let box: HTMLDivElement;
107 const scrollToBottom = () => {
108 if (!box) return;
109
110 box.scroll({
111 top: box.scrollHeight,
112 behavior: 'smooth'
113 });
114 };
115 afterUpdate(scrollToBottom);
116</script>
117
118<div bind:this={box} class="flex size-full flex-col gap-2 overflow-x-hidden overflow-y-scroll pr-2">
119 {#each events as event}
120 <div class="relative w-full" transition:fly={{ x: 30, duration: 1000 }}>
121 {#if isScore(event)}
122 <EventScore
123 score={event.data.score}
124 square={event.data.square}
125 square_index={event.data.square_index}
126 stat={$game_rules?.reclaim_condition ?? ''}
127 />
128 {:else if isGameEvent(event)}
129 {#if new Date(event.date).valueOf() < new Date().valueOf()}
130 <EventTime event={event.data} />
131 {/if}
132 {:else if isStart(event)}
133 <EventStart />
134 {/if}
135 </div>
136 {/each}
137</div>