diff --git a/public/locales/de.json b/public/locales/de.json index c6f129c..1fc9f82 100644 --- a/public/locales/de.json +++ b/public/locales/de.json @@ -6,6 +6,7 @@ "Dialog.advanced.displayDuration": "Anzeigedauer für Overlay", "Dialog.advanced.displayDurationUnit": "Sek.", "Dialog.advanced.otherOptions": "Weitere Optionen", + "Dialog.advanced.playerStatus": "Spielerstatus anzeigen", "Dialog.advanced.reduceAnimations": "Animationen reduzieren", "Dialog.advanced.colorLock": "Farbhilfe", diff --git a/public/locales/en.json b/public/locales/en.json index bc40e80..bc16780 100644 --- a/public/locales/en.json +++ b/public/locales/en.json @@ -6,6 +6,7 @@ "Dialog.advanced.displayDuration": "Display Duration for Overlay", "Dialog.advanced.displayDurationUnit": "sec", "Dialog.advanced.otherOptions": "Other Options", + "Dialog.advanced.playerStatus": "Show Player Status", "Dialog.advanced.reduceAnimations": "Reduce Animations", "Dialog.advanced.colorLock": "Color Lock", diff --git a/public/locales/es-419.json b/public/locales/es-419.json index 1aa76ea..4e25b42 100644 --- a/public/locales/es-419.json +++ b/public/locales/es-419.json @@ -6,6 +6,7 @@ "Dialog.advanced.displayDuration": "Duración de visualización para la superposición", "Dialog.advanced.displayDurationUnit": "seg", "Dialog.advanced.otherOptions": "Otras opciones", + "Dialog.advanced.playerStatus": "Mostrar estado de los jugadores", "Dialog.advanced.reduceAnimations": "Reducir animaciones", "Dialog.advanced.colorLock": "Ayuda de colores", diff --git a/public/locales/es.json b/public/locales/es.json index 92a985a..8721be5 100644 --- a/public/locales/es.json +++ b/public/locales/es.json @@ -6,6 +6,7 @@ "Dialog.advanced.displayDuration": "Duración de visualización para la superposición", "Dialog.advanced.displayDurationUnit": "seg", "Dialog.advanced.otherOptions": "Otras opciones", + "Dialog.advanced.playerStatus": "Mostrar estado de los jugadores", "Dialog.advanced.reduceAnimations": "Reducir animaciones", "Dialog.advanced.colorLock": "Ayuda de colores", diff --git a/public/locales/fr-CA.json b/public/locales/fr-CA.json index ea6dc91..ddd0b9a 100644 --- a/public/locales/fr-CA.json +++ b/public/locales/fr-CA.json @@ -6,6 +6,7 @@ "Dialog.advanced.displayDuration": "Durée d'affichage pour la superposition", "Dialog.advanced.displayDurationUnit": "sec", "Dialog.advanced.otherOptions": "Autres options", + "Dialog.advanced.playerStatus": "Afficher le statut des joueurs", "Dialog.advanced.reduceAnimations": "Réduire les animations", "Dialog.advanced.colorLock": "Verrou couleurs", diff --git a/public/locales/fr.json b/public/locales/fr.json index d127ac5..3a4b356 100644 --- a/public/locales/fr.json +++ b/public/locales/fr.json @@ -6,6 +6,7 @@ "Dialog.advanced.displayDuration": "Durée d'affichage pour la superposition", "Dialog.advanced.displayDurationUnit": "sec", "Dialog.advanced.otherOptions": "Autres options", + "Dialog.advanced.playerStatus": "Afficher le statut des joueurs", "Dialog.advanced.reduceAnimations": "Réduire les animations", "Dialog.advanced.colorLock": "Verrou couleurs", diff --git a/public/locales/it.json b/public/locales/it.json index 189b832..75b9e4e 100644 --- a/public/locales/it.json +++ b/public/locales/it.json @@ -6,6 +6,7 @@ "Dialog.advanced.displayDuration": "Durata di visualizzazione per la sovrapposizione", "Dialog.advanced.displayDurationUnit": "sec", "Dialog.advanced.otherOptions": "Altre opzioni", + "Dialog.advanced.playerStatus": "Mostra stato dei giocatori", "Dialog.advanced.reduceAnimations": "Riduci le animazioni", "Dialog.advanced.colorLock": "Aiuto colore", diff --git a/public/locales/ja.json b/public/locales/ja.json index ff390f7..f8dde00 100644 --- a/public/locales/ja.json +++ b/public/locales/ja.json @@ -6,6 +6,7 @@ "Dialog.advanced.displayDuration": "オーバーレイの表示時間", "Dialog.advanced.displayDurationUnit": "秒", "Dialog.advanced.otherOptions": "その他のオプション", + "Dialog.advanced.playerStatus": "プレイヤー状況を表示する", "Dialog.advanced.reduceAnimations": "アニメーションを減らす", "Dialog.advanced.colorLock": "色覚サポート", diff --git a/public/locales/ko.json b/public/locales/ko.json index 6c6d985..5c3f857 100644 --- a/public/locales/ko.json +++ b/public/locales/ko.json @@ -6,6 +6,7 @@ "Dialog.advanced.displayDuration": "오버레이 표시 시간", "Dialog.advanced.displayDurationUnit": "초", "Dialog.advanced.otherOptions": "기타 옵션", + "Dialog.advanced.playerStatus": "플레이어 상태 표시", "Dialog.advanced.reduceAnimations": "애니메이션 줄이기", "Dialog.advanced.colorLock": "색각 서포트", diff --git a/public/locales/nl.json b/public/locales/nl.json index ae5c84b..13b136c 100644 --- a/public/locales/nl.json +++ b/public/locales/nl.json @@ -6,6 +6,7 @@ "Dialog.advanced.displayDuration": "Weergaveduur voor overlay", "Dialog.advanced.displayDurationUnit": "sec", "Dialog.advanced.otherOptions": "Andere Opties", + "Dialog.advanced.playerStatus": "Toon spelersstatus", "Dialog.advanced.reduceAnimations": "Animaties verminderen", "Dialog.advanced.colorLock": "Vaste kleuren", diff --git a/public/locales/ru.json b/public/locales/ru.json index c1826b0..5c6bdef 100644 --- a/public/locales/ru.json +++ b/public/locales/ru.json @@ -6,6 +6,7 @@ "Dialog.advanced.displayDuration": "Время отображения наложения", "Dialog.advanced.displayDurationUnit": "сек", "Dialog.advanced.otherOptions": "Другие варианты", + "Dialog.advanced.playerStatus": "Показать статус игроков", "Dialog.advanced.reduceAnimations": "Уменьшить анимации", "Dialog.advanced.colorLock": "Ограничение цветов", diff --git a/public/locales/zh-CN.json b/public/locales/zh-CN.json index 2626211..f7cebfe 100644 --- a/public/locales/zh-CN.json +++ b/public/locales/zh-CN.json @@ -6,6 +6,7 @@ "Dialog.advanced.displayDuration": "叠加显示时长", "Dialog.advanced.displayDurationUnit": "秒", "Dialog.advanced.otherOptions": "其他选项", + "Dialog.advanced.playerStatus": "显示玩家状态", "Dialog.advanced.reduceAnimations": "减少动画", "Dialog.advanced.colorLock": "色彩辅助辨识", diff --git a/public/locales/zh-TW.json b/public/locales/zh-TW.json index 7bf740f..17bea26 100644 --- a/public/locales/zh-TW.json +++ b/public/locales/zh-TW.json @@ -6,6 +6,7 @@ "Dialog.advanced.displayDuration": "疊加顯示時長", "Dialog.advanced.displayDurationUnit": "秒", "Dialog.advanced.otherOptions": "其他選項", + "Dialog.advanced.playerStatus": "顯示玩家狀態", "Dialog.advanced.reduceAnimations": "減少動畫", "Dialog.advanced.colorLock": "色彩辨識輔助", diff --git a/src/modules/core/components/EggGraph/YAxis.tsx b/src/modules/core/components/EggGraph/YAxis.tsx index f4f0b12..e16d749 100644 --- a/src/modules/core/components/EggGraph/YAxis.tsx +++ b/src/modules/core/components/EggGraph/YAxis.tsx @@ -15,7 +15,7 @@ interface YAxisProps { } const amountLabelProps: TickLabelProps>> = Object.freeze({ - dx: '-.25em', + dx: -4, dy: '.4em', fontFamily: undefined, fontSize: undefined, diff --git a/src/modules/core/components/EggGraph/index.tsx b/src/modules/core/components/EggGraph/index.tsx index c171dad..cbda1bb 100644 --- a/src/modules/core/components/EggGraph/index.tsx +++ b/src/modules/core/components/EggGraph/index.tsx @@ -7,18 +7,21 @@ import { curveLinear } from '@visx/curve' import { scaleLinear } from '@visx/scale' import { LinePath } from '@visx/shape' -import { getSvgProps, type GraphLayoutProps } from '@/core/models/graph' +import { getSvgProps, type GraphRootProps } from '@/core/models/graph' import { forceLast } from '@/core/utils/collection' import type { ShakeDefaultWave, ShakeTelemetry } from '@/telemetry/models/data' +import PlayerStatus, { PlayerStatusGraphHeight } from '../PlayerStatus' + import HLine from './HLine' import Header from './Header' import XAxis from './XAxis' import YAxis from './YAxis' import EggGraphMessages from './messages' -export interface EggGraphProps extends GraphLayoutProps { +export interface EggGraphProps extends GraphRootProps { readonly colorLock?: boolean + readonly status?: boolean readonly telemetry?: Readonly readonly wave?: Readonly } @@ -27,10 +30,12 @@ const EggGraph = function (props: EggGraphProps) { const { marginTop: y, marginLeft: x, + marginRight, width, height, colorLock, + status: statusVisible, telemetry, wave: waveData, } = props @@ -40,8 +45,9 @@ const EggGraph = function (props: EggGraphProps) { const intl = useIntl() + const offsetY = statusVisible ? PlayerStatusGraphHeight + 16 : 0 const svgProps = getSvgProps(props) - const transform = `translate(${x},${y})` + const transform = `translate(${x},${y + offsetY})` const lastUpdate = forceLast(waveData.updates) const status = waveData.quota <= lastUpdate.amount @@ -52,11 +58,12 @@ const EggGraph = function (props: EggGraphProps) { ? false : undefined const maxY = Math.max(5 * Math.ceil(0.2 * waveData.quota), lastUpdate.amount) + const graphHeight = height - offsetY const amountScale = useMemo(() => scaleLinear({ domain: [maxY, 0], - range: [0, height], + range: [0, graphHeight], nice: true, - }), [height, maxY]) + }), [graphHeight, maxY]) const countScale = useMemo(() => scaleLinear({ domain: [100, 0], range: [0, width], @@ -75,19 +82,29 @@ const EggGraph = function (props: EggGraphProps) { /> + + +} + +const PlayerStatusItem = function ({ y, start, player, scale }: PlayerStatusItemProps) { + return ( + + + + # + {player.index + 1} + + {player.alives.map(function (alive) { + return ( + + + + + ) + })} + {player.geggs.map(function (gegg) { + return ( + + ) + })} + + ) +} + +export default memo(PlayerStatusItem) diff --git a/src/modules/core/components/PlayerStatus/index.tsx b/src/modules/core/components/PlayerStatus/index.tsx new file mode 100644 index 0000000..68f9600 --- /dev/null +++ b/src/modules/core/components/PlayerStatus/index.tsx @@ -0,0 +1,55 @@ +import { ScaleLinear } from 'd3-scale' + +import { ShakeDefaultWave } from '@/telemetry/models/data' + +import { GraphLayoutProps } from '../../models/graph' + +import PlayerStatusItem from './PlayerStatusItem' + +// Spacing between player status item and golden egg graph +const playerStatusItemHeight = 20 +const playerStatusItemHeightHalf = 0.5 * playerStatusItemHeight + +export const PlayerStatusGraphHeight = 4 * playerStatusItemHeight + +interface PlayerStatusProps extends Omit { + readonly data: Readonly + readonly scale: ScaleLinear + readonly visible?: boolean +} + +const PlayerStatus = function (props: PlayerStatusProps) { + const { + marginTop: y, + marginLeft: x, + width, + + data, + scale, + visible, + } = props + + return visible && ( + + + + {data.players.map(function (p, i) { + return ( + + ) + })} + + ) +} + +export default PlayerStatus diff --git a/src/modules/core/models/graph.ts b/src/modules/core/models/graph.ts index 00ff92d..cb83f0d 100644 --- a/src/modules/core/models/graph.ts +++ b/src/modules/core/models/graph.ts @@ -3,11 +3,14 @@ import { SVGProps } from 'react' export interface GraphLayoutProps { readonly marginTop: number readonly marginLeft: number - readonly marginRight: number - readonly marginBottom: number + readonly marginRight?: number + readonly marginBottom?: number readonly width: number readonly height: number +} + +export interface GraphRootProps extends GraphLayoutProps { readonly containerWidth?: number | string readonly containerHeight?: number | string } @@ -16,9 +19,9 @@ export type GraphSvgSizeProps = & Required, 'width' | 'height'>> & Pick, 'preserveAspectRatio' | 'viewBox'> -export function getSvgProps(props: GraphLayoutProps): GraphSvgSizeProps { - const graphWidth = props.width + props.marginLeft + props.marginRight - const graphHeight = props.height + props.marginTop + props.marginBottom +export function getSvgProps(props: GraphRootProps): GraphSvgSizeProps { + const graphWidth = props.width + props.marginLeft + (props.marginRight ?? 0) + const graphHeight = props.height + props.marginTop + (props.marginBottom ?? 0) if (!props.containerWidth || !props.containerHeight) { return Object.freeze({ diff --git a/src/modules/overlay/components/OverlayEggGraph/index.tsx b/src/modules/overlay/components/OverlayEggGraph/index.tsx index 230ca55..970a96a 100644 --- a/src/modules/overlay/components/OverlayEggGraph/index.tsx +++ b/src/modules/overlay/components/OverlayEggGraph/index.tsx @@ -14,21 +14,33 @@ import { RightSlideAnimation } from '../SlideAnimation' const preferredGraphLayout = Object.freeze({ marginTop: 48, // 3.0em in 720p (1em = 16px @ 720p, 1em = 24px @ 1080p) marginLeft: 44, // 2.75em in 720p - marginRight: 16, // 1.0em in 720p, with 2.25em padding + marginRight: 12, // 0.75em in 720p, with 3em padding marginBottom: 32, // 2.0em in 720p - width: 320 - 44 - 20, + width: 320 - 44 - 12, height: 192 - 48 - 32, containerWidth: '100%', containerHeight: '100%', } satisfies GraphLayoutProps) +const preferredGraphLayoutWithStatus = Object.freeze({ + marginTop: 48, // 3.0em in 720p (1em = 16px @ 720p, 1em = 24px @ 1080p) + marginLeft: 44, // 2.75em in 720p + marginRight: 12, // 0.75em in 720p, with 3em padding + marginBottom: 32, // 2.0em in 720p + + width: 352 - 44 - 12, + height: 288 - 48 - 32, + containerWidth: '100%', + containerHeight: '100%', +} satisfies GraphLayoutProps) + type OverlayEggGraphProps = & { readonly visible: boolean } & Omit export const OverlayEggGraph = function (props: OverlayEggGraphProps) { - const { visible, ...nextProps } = props + const { status, visible, ...nextProps } = props const ref = useRef(null) return ( -
- +
+
) @@ -49,9 +65,11 @@ function mapStateToProps(state: RootState) { if (wave?.wave === 'extra') { wave = undefined } + return { colorLock: state.config.colorLock, telemetry, + status: state.config.status ?? true, visible: telemetry !== undefined && wave !== undefined, wave, } satisfies OverlayEggGraphProps diff --git a/src/modules/overlay/components/OverlayEggGraph/styles.css b/src/modules/overlay/components/OverlayEggGraph/styles.css index 3fc5bef..ebcd56a 100644 --- a/src/modules/overlay/components/OverlayEggGraph/styles.css +++ b/src/modules/overlay/components/OverlayEggGraph/styles.css @@ -1,10 +1,15 @@ .OverlayEggGraph { + --oleg-height: 12em; contain: strict; position: absolute; - top: 16.5em; + top: calc(28.5em - var(--oleg-height)); right: -.75em; width: 20em; - height: 12em; + height: var(--oleg-height); - padding-right: 2.25em; + padding-right: 3em; +} +.OverlayEggGraph-status { + --oleg-height: 18em; + width: 22em; } diff --git a/src/modules/overlay/components/OverlayHost/styles.css b/src/modules/overlay/components/OverlayHost/styles.css index 5d67be9..eb83753 100644 --- a/src/modules/overlay/components/OverlayHost/styles.css +++ b/src/modules/overlay/components/OverlayHost/styles.css @@ -34,23 +34,20 @@ .Overlay { background-color: var(--ol-bg); - transition: transform var(--animation-enter); } .Overlay-left { border-radius: 0 1.5em 1.5em 0; + transform: rotate(-3deg); } -.Overlay-left, .Overlay-left--slide-enter { transform: rotate(-3deg) translateX(-100%); } +.Overlay-left--slide-exit, .Overlay-left--slide-enter-active, .Overlay-left--slide-enter-done { transform: rotate(-3deg); } -.Overlay-left--slide-exit { - transform: rotate(-3deg); -} .Overlay-left--slide-exit-active, .Overlay-left--slide-exit-done { transform: rotate(-3deg) translateX(-100%); @@ -58,19 +55,24 @@ .Overlay-right { border-radius: 1.5em 0 0 1.5em; + transform: rotate(3deg); } -.Overlay-right, .Overlay-right--slide-enter { transform: rotate(3deg) translateX(100%); } +.Overlay-right--slide-exit, .Overlay-right--slide-enter-active, .Overlay-right--slide-enter-done { transform: rotate(3deg); } -.Overlay-right--slide-exit { - transform: rotate(3deg); -} .Overlay-right--slide-exit-active, .Overlay-right--slide-exit-done { transform: rotate(3deg) translateX(100%); } + +.Overlay-left--slide-enter-active, +.Overlay-left--slide-exit-active, +.Overlay-right--slide-enter-active, +.Overlay-right--slide-exit-active { + transition: transform var(--animation-enter); +} diff --git a/src/modules/overlay/components/ProductLogo/index.tsx b/src/modules/overlay/components/ProductLogo/index.tsx index 5d5cad9..e6f9ae3 100644 --- a/src/modules/overlay/components/ProductLogo/index.tsx +++ b/src/modules/overlay/components/ProductLogo/index.tsx @@ -11,7 +11,7 @@ const ProductLogo = () => { const ref = useRef(null) return ( -
+
Powered By
diff --git a/src/modules/settings/messages.ts b/src/modules/settings/messages.ts index 2467e9d..dcf2fdc 100644 --- a/src/modules/settings/messages.ts +++ b/src/modules/settings/messages.ts @@ -34,6 +34,10 @@ const DialogMessages = defineMessages({ id: 'Dialog.advanced.colorLock', defaultMessage: 'Color Lock', }, + advancedPlayerStatus: { + id: 'Dialog.advanced.playerStatus', + defaultMessage: 'Show Player Status', + }, dataSource: { id: 'Dialog.dataSource', diff --git a/src/modules/settings/pages/AdvancedPage.tsx b/src/modules/settings/pages/AdvancedPage.tsx index d236953..2158934 100644 --- a/src/modules/settings/pages/AdvancedPage.tsx +++ b/src/modules/settings/pages/AdvancedPage.tsx @@ -14,6 +14,7 @@ import { setNotifyOnQuotaMetDuration, setNotifyOnWaveFinishedDuration, setReduced, + setStatus, } from '../slicers' const AdvancedPage = () => { @@ -34,6 +35,7 @@ const AdvancedPage = () => { } const colorLocked = useAppSelector(state => state.config.colorLock) ?? false + const playerStatusEnabled = useAppSelector(state => state.config.status) ?? false const dispatch = useDispatch() const handleNotifyOnQuotaMetDuration = useCallback((notifyOnQuotaMetDuration: number) => { @@ -48,6 +50,9 @@ const AdvancedPage = () => { const handleColorLock = useCallback((colorLocked: boolean) => { dispatch(setColorLock(colorLocked)) }, [dispatch]) + const handlePlayerStatus = useCallback((playerStatusEnabled: boolean) => { + dispatch(setStatus(playerStatusEnabled)) + }, [dispatch]) return ( <> @@ -89,6 +94,13 @@ const AdvancedPage = () => {

{intl.formatMessage(DialogMessages.advancedOtherOptions)}

+ + {intl.formatMessage(DialogMessages.advancedPlayerStatus)} + ) { state.speed = action.payload }, + setStatus(state, action: PayloadAction) { + state.status = action.payload + }, }, }) @@ -91,5 +95,6 @@ export const { setServer, setSimulation, setSpeed, + setStatus, } = configSlice.actions export default persistConfigReducer diff --git a/src/modules/telemetry/models/data.ts b/src/modules/telemetry/models/data.ts index b687dd3..be405db 100644 --- a/src/modules/telemetry/models/data.ts +++ b/src/modules/telemetry/models/data.ts @@ -2,17 +2,30 @@ import { DefaultWaveStringType, DefaultWaveType, WaveType } from '@/core/utils/w import { ShakeCloseReason, ShakeColor, ShakeKing, ShakeStage } from './constant' +export interface ShakePlayerStatus { + readonly alive: boolean + readonly gegg: boolean +} + export interface ShakeUpdate { readonly timestamp: number readonly count: number readonly amount: number + readonly players: ShakePlayerStatus[] readonly unstable: boolean } -interface ShakeBaseWave { +export interface ShakePlayerWave { + readonly index: number + readonly alives: readonly (readonly [number, number])[] + readonly geggs: readonly (readonly [number, number])[] +} + +export interface ShakeBaseWave { readonly wave: WaveType startTimestamp: number endTimestamp?: number + readonly players: ShakePlayerWave[] } export interface ShakeDefaultWave extends ShakeBaseWave { diff --git a/src/modules/telemetry/models/telemetry.ts b/src/modules/telemetry/models/telemetry.ts index 6962788..6857887 100644 --- a/src/modules/telemetry/models/telemetry.ts +++ b/src/modules/telemetry/models/telemetry.ts @@ -1,46 +1,52 @@ import { ShakeColor, ShakeKing, ShakeStage } from './constant' export interface ShakeBaseEvent { - session: string - event: string - timestamp: number + readonly session: string + readonly event: string + readonly timestamp: number } export type ShakeMatchmakingEvent = ShakeBaseEvent & { - event: 'matchmaking' + readonly event: 'matchmaking' } export type ShakeGameStageEvent = ShakeBaseEvent & { - event: 'game_stage' - stage: ShakeStage + readonly event: 'game_stage' + readonly stage: ShakeStage } export type ShakeGameKingEvent = ShakeBaseEvent & { - event: 'game_king' - king: ShakeKing + readonly event: 'game_king' + readonly king: ShakeKing +} + +export interface ShakeGamePlayerUpdateEvent { + readonly alive: boolean + readonly gegg: boolean } export type ShakeGameUpdateEvent = ShakeBaseEvent & { - event: 'game_update' - color: ShakeColor - count?: number - unstable: boolean + readonly event: 'game_update' + readonly color: ShakeColor + readonly count?: number + readonly players: ShakeGamePlayerUpdateEvent[] + readonly unstable: boolean } & ({ - wave?: number - amount?: number - quota: number + readonly wave?: number + readonly amount?: number + readonly quota: number } | { - wave: 'extra' + readonly wave: 'extra' }) export type ShakeGameResultEvent = ShakeBaseEvent & { - event: 'game_result' - golden: number - power: number + readonly event: 'game_result' + readonly golden: number + readonly power: number } export type ShakeGameErrorEvent = ShakeBaseEvent & { - event: 'game_error' + readonly event: 'game_error' } export type ShakeEvent = diff --git a/src/modules/telemetry/slicers/index.ts b/src/modules/telemetry/slicers/index.ts index 448b6f1..3a7002d 100644 --- a/src/modules/telemetry/slicers/index.ts +++ b/src/modules/telemetry/slicers/index.ts @@ -4,7 +4,7 @@ import { WritableDraft, produce } from 'immer' import { getMatchFromTelemetry } from '../utils/getMatchFromTelemetry' import { TelemetryProcessor } from '../utils/processor' -import type { ShakeDefaultWave, ShakeExtraWave, ShakeTelemetry, ShakeUpdate, ShakeWaveRecord } from '../models/data' +import type { ShakeBaseWave, ShakeDefaultWave, ShakeExtraWave, ShakeTelemetry, ShakeUpdate, ShakeWaveRecord } from '../models/data' import type { ShakeMatch } from '../models/match' import type { ShakeEvent } from '../models/telemetry' @@ -37,29 +37,76 @@ const produceUpdates = ( } } +function produceStatus( + draft: WritableDraft<[number, number][]>, + payload: readonly (readonly [number, number])[], +) { + let i = 0 + for (; i < draft.length; ++i) { + if (!payload[i].equals(draft[i])) { + draft[i] = payload[i].slice(0) as [number, number] + } + } + for (; i < payload.length; ++i) { + draft[i] = payload[i].slice(0) as [number, number] + } +} + function produceWave( + draft: WritableDraft>, + payload: Readonly, +) { + const { players } = payload + for (let i = 0; i < 4; ++i) { + draft.players[i].index = players[i].index + produceStatus(draft.players[i].alives, players[i].alives) + produceStatus(draft.players[i].geggs, players[i].geggs) + } +} + +function produceDefaultWave( draft: WritableDraft>, payload: Readonly, ) { - const { updates, ...props } = payload - produceUpdates((draft as WritableDraft>).updates, updates) + const { updates, players, ...props } = payload + produceUpdates(draft.updates, updates) + produceWave(draft, payload) Object.assign(draft, props) } function produceWaves(draft: WritableDraft, payload: Readonly) { for (const [waveKey, wavePayload] of Object.entries(payload)) { if (Object.hasOwn(draft, waveKey)) { - if (waveKey !== 'extra') { - produceWave(draft[waveKey]!, wavePayload as Readonly) + if (waveKey === 'extra') { + produceWave(draft['extra']!, wavePayload as Readonly) + } else { + produceDefaultWave(draft[waveKey]!, wavePayload as Readonly) } } else { if (waveKey === 'extra') { - draft['extra'] = wavePayload as Readonly + const { players, ...props } = wavePayload as Readonly + draft['extra'] = { + ...props, + players: players.map(function (p) { + return { + index: p.index, + alives: p.alives.slice(0) as [number, number][], + geggs: p.geggs.slice(0) as [number, number][], + } + }), + } } else { - const { updates, ...props } = wavePayload as Readonly + const { updates, players, ...props } = wavePayload as Readonly draft[waveKey] = { ...props, updates: updates.map(u => Object.assign({}, u)), + players: players.map(function (p) { + return { + index: p.index, + alives: p.alives.slice(0) as [number, number][], + geggs: p.geggs.slice(0) as [number, number][], + } + }), } } } @@ -100,16 +147,16 @@ const telemetrySlice = createSlice({ waves: Object.entries(waves).reduce((draftWaves, pair) => { const [waveKey, wave] = pair if (waveKey === 'extra') { - draftWaves['extra'] = wave as ShakeExtraWave + draftWaves['extra'] = wave as WritableDraft } else { const { updates, ...props } = wave as ShakeDefaultWave draftWaves[waveKey] = { ...props, updates: updates.map(u => Object.assign({}, u)), - } + } as WritableDraft } return draftWaves - }, {} as ShakeWaveRecord), + }, {} as WritableDraft), } const match = getMatchFromTelemetry(newTelemetry) @@ -139,7 +186,7 @@ const telemetrySlice = createSlice({ if (temporary.isNewContext()) { const telemetry = temporary.current - state.entities[telemetry.id] = telemetry + state.entities[telemetry.id] = telemetry as WritableDraft const match = getMatchFromTelemetry(telemetry) state.matches.push(match) diff --git a/src/modules/telemetry/utils/IntervalProcessor.ts b/src/modules/telemetry/utils/IntervalProcessor.ts new file mode 100644 index 0000000..c6c2c11 --- /dev/null +++ b/src/modules/telemetry/utils/IntervalProcessor.ts @@ -0,0 +1,52 @@ +export class IntervalProcessor { + readonly #intervals: (readonly [number, number])[] = [] + + #startTimestamp: number | undefined = undefined + #totalDuration: number = 0 + + reset(): IntervalProcessor { + this.#intervals.length = 0 + this.#startTimestamp = undefined + this.#totalDuration = 0 + return this + } + + finalize(timestamp: number): void { + if (this.#startTimestamp !== undefined) { + const timestamps = Object.freeze([this.#startTimestamp, timestamp] satisfies [number, number]) + this.#intervals.push(timestamps) + this.#totalDuration += timestamp - this.#startTimestamp + this.#startTimestamp = undefined + } + } + + add(timestamp: number, entry: boolean): IntervalProcessor { + if (entry) { + if (this.#startTimestamp === undefined) { + this.#startTimestamp = timestamp + } + } else { + this.finalize(timestamp) + } + return this + } + + get(timestamp: number): readonly (readonly [number, number])[] { + if (this.#startTimestamp === undefined) { + return this.#intervals.slice(0) + } + + const intervals = this.#intervals.slice(0) + const timestamps = Object.freeze([this.#startTimestamp, timestamp] satisfies [number, number]) + intervals.push(timestamps) + return intervals + } + + get intervals(): readonly (readonly [number, number])[] { + return this.#intervals + } + + get totalDuration(): number { + return this.#totalDuration + } +} diff --git a/src/modules/telemetry/utils/processor.ts b/src/modules/telemetry/utils/processor.ts index 1142306..091fb61 100644 --- a/src/modules/telemetry/utils/processor.ts +++ b/src/modules/telemetry/utils/processor.ts @@ -1,11 +1,12 @@ -import { DefaultWaveType, isDefaultWave } from '@/core/utils/wave' - -import { ShakeDefaultWave, ShakeExtraWave, ShakeTelemetry, ShakeUpdate } from '../models/data' -import { ShakeEvent, ShakeGameUpdateEvent } from '../models/telemetry' +import { type WaveType, isDefaultWave } from '@/core/utils/wave' import { CounterAnomalyDetector } from './CounterAnomalyDetector' +import { IntervalProcessor } from './IntervalProcessor' import { FrequencyCounter } from './frequencyCounter' +import type { ShakeBaseWave, ShakeDefaultWave, ShakeExtraWave, ShakeTelemetry, ShakeUpdate } from '../models/data' +import type { ShakeEvent, ShakeGameKingEvent, ShakeGameUpdateEvent } from '../models/telemetry' + export class TelemetryProcessor { readonly #maxIncreaseAmountInSeconds: number @@ -15,19 +16,25 @@ export class TelemetryProcessor { // Data processing instances readonly #quotaCounter: FrequencyCounter = new FrequencyCounter() readonly #countAnomalyDetector: CounterAnomalyDetector = new CounterAnomalyDetector() + readonly #players: readonly [IntervalProcessor, IntervalProcessor][] = [ + [new IntervalProcessor(), new IntervalProcessor()], + [new IntervalProcessor(), new IntervalProcessor()], + [new IntervalProcessor(), new IntervalProcessor()], + [new IntervalProcessor(), new IntervalProcessor()], + ] // Base count; use this value as base of this.#waveData.startTimestamp #baseCount: number = 110 // Current wave - #currentWave: DefaultWaveType | undefined = undefined + #currentWave: WaveType | undefined = undefined // Raw event storage readonly #storage: ShakeEvent[] = [] // Telemetry data #data: ShakeTelemetry = undefined as any - #waveData: ShakeDefaultWave = undefined as any + #waveData: ShakeBaseWave = undefined as any constructor(maxIncreaseAmountInSeconds: number) { this.#maxIncreaseAmountInSeconds = maxIncreaseAmountInSeconds @@ -49,6 +56,10 @@ export class TelemetryProcessor { this.#countAnomalyDetector.reset() this.#quotaCounter.reset() + this.#players.forEach(function ([alive, gegg]) { + alive.reset() + gegg.reset() + }) this.#baseCount = 110 this.#currentWave = undefined @@ -62,11 +73,57 @@ export class TelemetryProcessor { this.#waveData = undefined as any } + #processInitExtra(ev: Readonly) { + this.#countAnomalyDetector.reset() + this.#baseCount = 100 + this.#currentWave = 'extra' + + // Create new wave data + const newWaveData: ShakeExtraWave = { + wave: 'extra', + king: ev.king, + startTimestamp: ev.timestamp, + players: this.#players.map(function ([alive, gegg], index) { + return { + index, + alives: alive.reset().add(ev.timestamp, true).get(ev.timestamp), + geggs: gegg.reset().add(ev.timestamp, false).get(ev.timestamp), + } + }), + } as const + this.#data.waveKeys.push('extra') + this.#data.waves['extra'] = newWaveData + this.#waveData = newWaveData + } + + #processUpdateExtra(ev: Readonly) { + if (ev.count === undefined) { + return // DISPOSE!! + } + + // Update wave + const currentWaveData = this.#waveData as ShakeExtraWave + + // Update quota and status + this.#players.forEach(function ([alive, gegg], index) { + const pev = ev.players[index] + currentWaveData.players[index] = { + index, + alives: alive.add(ev.timestamp, pev.alive).get(ev.timestamp), + geggs: gegg.add(ev.timestamp, pev.gegg).get(ev.timestamp), + } + }) + } + #processUpdate(ev: Readonly) { // Extra wave if (ev.wave === 'extra') { + this.#processUpdateExtra(ev) return } + if (this.#currentWave === 'extra') { + throw Error('Current wave is extra.') + } // Set color if (this.#currentWave === undefined) { @@ -117,9 +174,18 @@ export class TelemetryProcessor { timestamp: ev.timestamp, count: ev.count, amount: ev.amount ?? 0 /* MAYBE 0 */, + players: ev.players, unstable: ev.unstable, }), ], + players: this.#players.map(function ([alive, gegg], index) { + const pev = ev.players[index] + return { + index, + alives: alive.reset().add(ev.timestamp, pev.alive).get(ev.timestamp), + geggs: gegg.reset().add(ev.timestamp, pev.gegg).get(ev.timestamp), + } + }), } as const this.#data.waveKeys.push(currentWave!) this.#data.waves[currentWave!] = newWaveData @@ -128,7 +194,7 @@ export class TelemetryProcessor { } // Update wave - const currentWaveData = this.#waveData + const currentWaveData = this.#waveData as ShakeDefaultWave let count = ev.count if (count === undefined) { @@ -159,8 +225,16 @@ export class TelemetryProcessor { } } - // Update quota + // Update quota and status currentWaveData.quota = this.#quotaCounter.add(ev.quota).mode + this.#players.forEach(function ([alive, gegg], index) { + const pev = ev.players[index] + currentWaveData.players[index] = { + index, + alives: alive.add(ev.timestamp, pev.alive).get(ev.timestamp), + geggs: gegg.add(ev.timestamp, pev.gegg).get(ev.timestamp), + } + }) // Check diff >= 0 const lastUpdate = currentWaveData.updates.at(-1) @@ -192,6 +266,7 @@ export class TelemetryProcessor { timestamp: ev.timestamp, count, amount: ev.amount, + players: ev.players, unstable: ev.unstable, }) currentWaveData.updates.push(newUpdateData) @@ -225,17 +300,10 @@ export class TelemetryProcessor { case 'game_stage': this.#data.stage = ev.stage break - case 'game_king': { - const newExtraWaveData: ShakeExtraWave = Object.freeze({ - wave: 'extra', - startTimestamp: ev.timestamp, - king: ev.king, - }) - this.#data.waveKeys.push('extra') - this.#data.waves['extra'] = newExtraWaveData + case 'game_king': + this.#processInitExtra(ev) break } - } } // Check new context diff --git a/types/collection.d.ts b/types/collection.d.ts index d226a81..1dfcaa5 100644 --- a/types/collection.d.ts +++ b/types/collection.d.ts @@ -3,3 +3,9 @@ interface Array { equals(other: Array): boolean } + +interface ReadonlyArray { + get last(): Readonly | undefined + + equals(other: ReadonlyArray): boolean +}