From 0192e1890ef404530e08d5fdc24f83523f150a0c Mon Sep 17 00:00:00 2001 From: Oliver Grack Date: Sun, 19 Jan 2025 23:52:47 +0100 Subject: [PATCH] fixed crystal guardian and enraged guardian not shown in splits view --- src/lib/parser/player-data/enemies.ts | 166 +-- .../recording-files/recording-splits.ts | 1076 +++++++++-------- src/lib/viz/store/store-context.tsx | 27 +- src/server/ingameauth/allow-login.ts | 3 +- 4 files changed, 660 insertions(+), 612 deletions(-) diff --git a/src/lib/parser/player-data/enemies.ts b/src/lib/parser/player-data/enemies.ts index 4b23ff81..3d73884c 100644 --- a/src/lib/parser/player-data/enemies.ts +++ b/src/lib/parser/player-data/enemies.ts @@ -1,24 +1,24 @@ import { type EnemyName, enemiesGenerated, enemiesJournalGenerated } from '../../hk-data'; export const greyPrinceNames = [ - // https://hollowknight.wiki/w/Grey_Prince_Zote - 'Terrifying, Beautiful, Powerful, Grey Prince Zote', - 'Gorgeous, Passionate,', - 'Diligent, Overwhelming,', - 'Vigorous,', - 'Enchanting,', - 'Mysterious,', - 'Sensual,', - 'Fearless,', - 'Invincible,', + // https://hollowknight.wiki/w/Grey_Prince_Zote + 'Terrifying, Beautiful, Powerful, Grey Prince Zote', + 'Gorgeous, Passionate,', + 'Diligent, Overwhelming,', + 'Vigorous,', + 'Enchanting,', + 'Mysterious,', + 'Sensual,', + 'Fearless,', + 'Invincible,', ]; for (let i = 1; i < greyPrinceNames.length; i++) { - greyPrinceNames[i] = greyPrinceNames[i] + ' ' + greyPrinceNames[i - 1]; + greyPrinceNames[i] = greyPrinceNames[i] + ' ' + greyPrinceNames[i - 1]; } for (let i = 0; i < greyPrinceNames.length; i++) { - greyPrinceNames[i] = greyPrinceNames[i] + ` (Level ${i + 1})`; + greyPrinceNames[i] = greyPrinceNames[i] + ` (Level ${i + 1})`; } // fields in playerData {name}Defeated: boolean @@ -27,69 +27,69 @@ for (let i = 0; i < greyPrinceNames.length; i++) { // create a single 'boss' like the Watcher Knights. // additionally the dreamers are detected like this. export const playerDataNameToDefeatedName: Record< - string, - { enemy: EnemyName; overrideName?: string } | { dreamer: string; achievementSprite: string } | undefined + string, + { enemy: EnemyName; overrideName?: string } | { dreamer: string; achievementSprite: string } | undefined > = { - // guardians would refer to the watcher knights but only becomes true once lurien is defeated, therefore undefined - guardians: undefined, // { enemy: 'BlackKnight', overrideName: 'Watcher Knights' }, - lurien: { dreamer: 'Lurien', achievementSprite: 'Achievement_icon__0000_watcher' }, - hegemol: { dreamer: 'Herrah', achievementSprite: 'Achievement_icon__0002_beast' }, - monomon: { dreamer: 'Monomon', achievementSprite: 'Achievement_icon__0001_teacher' }, - zote: undefined, // covered below { enemy: 'Zote' }, - falseKnight: undefined, - falseKnightDream: { enemy: 'FalseKnight', overrideName: 'Failed Champion' }, - mawlek: undefined, - giantBuzzer: undefined, - giantFly: undefined, - blocker1: undefined, - blocker2: undefined, - hornet1: { enemy: 'Hornet', overrideName: 'Hornet Protector' }, - collector: undefined, - hornetOutskirts: { enemy: 'Hornet', overrideName: 'Hornet Sentinel' }, - // dream bosses (some missing?) - mageLordDream: { enemy: 'MageLord', overrideName: 'Soul Tyrant' }, - infectedKnightDream: { enemy: 'InfectedKnight', overrideName: 'Lost Kin' }, - whiteDefender: undefined, // { enemy: 'DungDefender', overrideName: 'White Defender' }, - greyPrince: undefined, // { enemy: 'Zote', overrideName: 'Grey Prince Zote' }, - // dream warriors - aladarSlug: undefined, - xero: undefined, - elderHu: undefined, - mumCaterpillar: undefined, - noEyes: undefined, - markoth: undefined, - galien: undefined, - megaMossCharger: undefined, - mageLord: undefined, - // other bosses - flukeMother: undefined, - duskKnight: undefined, + // guardians would refer to the watcher knights but only becomes true once lurien is defeated, therefore undefined + guardians: undefined, // { enemy: 'BlackKnight', overrideName: 'Watcher Knights' }, + lurien: { dreamer: 'Lurien', achievementSprite: 'Achievement_icon__0000_watcher' }, + hegemol: { dreamer: 'Herrah', achievementSprite: 'Achievement_icon__0002_beast' }, + monomon: { dreamer: 'Monomon', achievementSprite: 'Achievement_icon__0001_teacher' }, + zote: undefined, // covered below { enemy: 'Zote' }, + falseKnight: undefined, + falseKnightDream: { enemy: 'FalseKnight', overrideName: 'Failed Champion' }, + mawlek: undefined, + giantBuzzer: undefined, + giantFly: undefined, + blocker1: undefined, + blocker2: undefined, + hornet1: { enemy: 'Hornet', overrideName: 'Hornet Protector' }, + collector: undefined, + hornetOutskirts: { enemy: 'Hornet', overrideName: 'Hornet Sentinel' }, + // dream bosses (some missing?) + mageLordDream: { enemy: 'MageLord', overrideName: 'Soul Tyrant' }, + infectedKnightDream: { enemy: 'InfectedKnight', overrideName: 'Lost Kin' }, + whiteDefender: undefined, // { enemy: 'DungDefender', overrideName: 'White Defender' }, + greyPrince: undefined, // { enemy: 'Zote', overrideName: 'Grey Prince Zote' }, + // dream warriors + aladarSlug: undefined, + xero: undefined, + elderHu: undefined, + mumCaterpillar: undefined, + noEyes: undefined, + markoth: undefined, + galien: undefined, + megaMossCharger: undefined, + mageLord: undefined, + // other bosses + flukeMother: undefined, + duskKnight: undefined, }; const journalInfoByPlayerDataName = Object.fromEntries( - enemiesJournalGenerated.map((journalInfo) => [journalInfo.playerDataName, journalInfo]), + enemiesJournalGenerated.map((journalInfo) => [journalInfo.playerDataName, journalInfo]), ); const enemyArray = Object.values(enemiesGenerated).map((enemy) => { - const journalInfo = journalInfoByPlayerDataName[enemy.name]; - return { - ...enemy, - portraitName: journalInfo?.portraitName, - convoName: journalInfo?.convoName, - descConvo: journalInfo?.descConvo, - nameConvo: journalInfo?.nameConvo, - notesConvo: journalInfo?.notesConvo, - playerDataBoolName: journalInfo?.playerDataBoolName, - playerDataKillsName: journalInfo?.playerDataKillsName, - playerDataName: journalInfo?.playerDataName, - playerDataNewDataName: journalInfo?.playerDataNewDataName, - }; + const journalInfo = journalInfoByPlayerDataName[enemy.name]; + return { + ...enemy, + portraitName: journalInfo?.portraitName, + convoName: journalInfo?.convoName, + descConvo: journalInfo?.descConvo, + nameConvo: journalInfo?.nameConvo, + notesConvo: journalInfo?.notesConvo, + playerDataBoolName: journalInfo?.playerDataBoolName, + playerDataKillsName: journalInfo?.playerDataKillsName, + playerDataName: journalInfo?.playerDataName, + playerDataNewDataName: journalInfo?.playerDataNewDataName, + }; }); export type EnemyInfo = (typeof enemyArray)[number]; export const enemies = { - byPlayerDataName: Object.fromEntries(enemyArray.map((enemy) => [enemy.name, enemy])), + byPlayerDataName: Object.fromEntries(enemyArray.map((enemy) => [enemy.name, enemy])), }; // By default all enemies which need to be killed only once @@ -97,28 +97,30 @@ export const enemies = { // of enemies which have only 1 kill, but are no bosses and the other way around // these exceptions are defined here const isBossOverrides: Partial> = { - BlackKnight: true, // Watcher Knights - BigFly: true, // Gruz Mother - MageKnight: true, // Soul Warrior - ZapBug: false, // Lumafly - Worm: false, // Goam - BigCentipede: false, // Garpede - AbyssTendril: false, // Void Tendrils - LazyFlyer: false, // Aluba - BindingSeal: false, + BlackKnight: true, // Watcher Knights + BigFly: true, // Gruz Mother + MageKnight: true, // Soul Warrior + ZapBug: false, // Lumafly + Worm: false, // Goam + BigCentipede: false, // Garpede + AbyssTendril: false, // Void Tendrils + LazyFlyer: false, // Aluba + BindingSeal: false, - ZotelingBuzzer: false, // Winged Zoteling - GreyPrince: false, - Zote: false, - ZotelingHopper: false, - ZotelingBalloon: false, + ZotelingBuzzer: false, // Winged Zoteling + GreyPrince: false, + Zote: false, + ZotelingHopper: false, + ZotelingBalloon: false, + + MegaBeamMiner: true, // Crystal Guardian }; export function isEnemyBoss(enemy: EnemyInfo): boolean { - const override = isBossOverrides[enemy.name]; - if (override !== undefined) { - return override; - } + const override = isBossOverrides[enemy.name]; + if (override !== undefined) { + return override; + } - return enemy.neededForJournal === 1; + return enemy.neededForJournal === 1; } diff --git a/src/lib/parser/recording-files/recording-splits.ts b/src/lib/parser/recording-files/recording-splits.ts index aeeeef98..e3d6560e 100644 --- a/src/lib/parser/recording-files/recording-splits.ts +++ b/src/lib/parser/recording-files/recording-splits.ts @@ -2,94 +2,94 @@ import { virtualCharms } from '../charms'; import { enemiesJournalLang } from '../../hk-data'; import { abilitiesAndItems, isPlayerDataAbilityOrItemField } from '../player-data/abilities'; import { - enemies, - greyPrinceNames, - isEnemyBoss, - playerDataNameToDefeatedName, - type EnemyInfo, + enemies, + greyPrinceNames, + isEnemyBoss, + playerDataNameToDefeatedName, + type EnemyInfo, } from '../player-data/enemies'; import { - getDefaultValue, - getEnemyNameFromDefeatedField, - getEnemyNameFromKilledField, - isPlayerDataBoolField, - isPlayerDataDefeatedField, - isPlayerDataKilledField, - playerDataFields, + getDefaultValue, + getEnemyNameFromDefeatedField, + getEnemyNameFromKilledField, + isPlayerDataBoolField, + isPlayerDataDefeatedField, + isPlayerDataKilledField, + playerDataFields, } from '../player-data/player-data'; import { type PlayerPositionEvent } from './events/player-position-event'; import { type CombinedRecording } from './recording'; import { assertNever, parseHtmlEntities } from '../util'; export const recordingSplitGroups = [ - { - name: 'dreamer', - displayName: 'Dreamers', - description: 'Broken Dreamer seals', - defaultShown: true, - }, - { - name: 'boss', - displayName: 'Bosses defeats', - description: 'Defeated bosses. Not including bosses which are fought again in Godhome.', - defaultShown: true, - }, - { - name: 'abilities', - displayName: 'Abilities', - description: 'Obtained abilities. (E.g. spells)', - defaultShown: true, - }, - { - name: 'items', - displayName: 'Items', - description: 'Collected items (e.g. the map or delicate flower). Not including charm collections and relicts.', - defaultShown: true, - }, - { - name: 'charmCollection', - displayName: 'Charm pick ups', - description: 'Collected charms and charm upgrades', - defaultShown: true, - }, + { + name: 'dreamer', + displayName: 'Dreamers', + description: 'Broken Dreamer seals', + defaultShown: true, + }, + { + name: 'boss', + displayName: 'Bosses defeats', + description: 'Defeated bosses. Not including bosses which are fought again in Godhome.', + defaultShown: true, + }, + { + name: 'abilities', + displayName: 'Abilities', + description: 'Obtained abilities. (E.g. spells)', + defaultShown: true, + }, + { + name: 'items', + displayName: 'Items', + description: 'Collected items (e.g. the map or delicate flower). Not including charm collections and relicts.', + defaultShown: true, + }, + { + name: 'charmCollection', + displayName: 'Charm pick ups', + description: 'Collected charms and charm upgrades', + defaultShown: true, + }, ] as const; export const recordingSplitGroupsByName = Object.fromEntries( - recordingSplitGroups.map((group) => [group.name, group]), + recordingSplitGroups.map((group) => [group.name, group]), ) as Record<(typeof recordingSplitGroups)[number]['name'], (typeof recordingSplitGroups)[number]>; export type RecordingSplitGroup = (typeof recordingSplitGroups)[number]; export type RecordingSplitGroupName = RecordingSplitGroup['name']; export interface RecordingSplit { - msIntoGame: number; - title: string; - tooltip: string; - imageUrl: string | undefined; - group: RecordingSplitGroup; - debugInfo: unknown; - previousPlayerPositionEvent: PlayerPositionEvent | null; + msIntoGame: number; + title: string; + tooltip: string; + imageUrl: string | undefined; + group: RecordingSplitGroup; + debugInfo: unknown; + previousPlayerPositionEvent: PlayerPositionEvent | null; } function createRecordingSplitFromEnemy( - msIntoGame: number, - enemyName: string, - enemyInfo: EnemyInfo | undefined, - previousPlayerPositionEvent: PlayerPositionEvent | null, - overrideName?: string | undefined, + msIntoGame: number, + enemyName: string, + enemyInfo: EnemyInfo | undefined, + previousPlayerPositionEvent: PlayerPositionEvent | null, + overrideName?: string | undefined, ): RecordingSplit { - const nameConvo = enemyInfo?.nameConvo; - const name = nameConvo ? enemiesJournalLang[nameConvo] : undefined; - const enemyNameDisplay = overrideName ?? (nameConvo && name ? parseHtmlEntities(name) ?? enemyName : enemyName); - return { - msIntoGame, - title: enemyNameDisplay, // + '(' + enemyInfo?.neededForJournal + ')', - tooltip: `Defeated ${enemyNameDisplay}`, - imageUrl: enemyInfo?.portraitName ? `/ingame-sprites/bestiary/${enemyInfo.portraitName}.png` : undefined, - group: recordingSplitGroupsByName.boss, - debugInfo: enemyInfo, - previousPlayerPositionEvent, - }; + const nameConvo = enemyInfo?.nameConvo; + const name = nameConvo ? enemiesJournalLang[nameConvo] : undefined; + const enemyNameDisplay = overrideName ?? (nameConvo && name ? (parseHtmlEntities(name) ?? enemyName) : enemyName); + return { + msIntoGame, + title: enemyNameDisplay, // + '(' + enemyInfo?.neededForJournal + ')', + tooltip: `Defeated ${enemyNameDisplay}`, + imageUrl: enemyInfo?.portraitName ? `/ingame-sprites/bestiary/${enemyInfo.portraitName}.png` : undefined, + group: recordingSplitGroupsByName.boss, + debugInfo: enemyInfo, + previousPlayerPositionEvent, + }; } // export class DefeatedRecordingSplit extends RecordingSplitBase { @@ -112,479 +112,501 @@ function createRecordingSplitFromEnemy( // } export function createRecordingSplits(recording: CombinedRecording): RecordingSplit[] { - const splits: RecordingSplit[] = []; + const splits: RecordingSplit[] = []; - for (const field of Object.values(playerDataFields.byFieldName)) { - if (isPlayerDataDefeatedField(field)) { - const enemyDefeatName = getEnemyNameFromDefeatedField(field); - const defeatMapping = playerDataNameToDefeatedName[enemyDefeatName]; - recording.allPlayerDataEventsOfField(field).forEach((event) => { - if (!(event.value && !event.previousPlayerDataEventOfField?.value)) { - return; - } + for (const field of Object.values(playerDataFields.byFieldName)) { + if (isPlayerDataDefeatedField(field)) { + const enemyDefeatName = getEnemyNameFromDefeatedField(field); + const defeatMapping = playerDataNameToDefeatedName[enemyDefeatName]; + recording.allPlayerDataEventsOfField(field).forEach((event) => { + if (!(event.value && !event.previousPlayerDataEventOfField?.value)) { + return; + } - if (defeatMapping === undefined) { - // ignore - // splits.push({ - // msIntoGame: event.msIntoGame, - // title: event.field.name, - // tooltip: 'Unknown', - // imageUrl: undefined, - // group: 'boss', - // debugInfo: undefined, - // }); - } else if ('enemy' in defeatMapping) { - const enemyInfo = enemies.byPlayerDataName[defeatMapping.enemy]; - splits.push( - createRecordingSplitFromEnemy( - event.msIntoGame, - enemyDefeatName, - enemyInfo, - event.previousPlayerPositionEvent, - defeatMapping.overrideName, - ), - ); - } else if ('dreamer' in defeatMapping) { - splits.push({ - msIntoGame: event.msIntoGame, - title: defeatMapping.dreamer, - tooltip: defeatMapping.dreamer + "'s Seal broken", - imageUrl: `/ingame-sprites/achievement/${defeatMapping.achievementSprite}.png`, - group: recordingSplitGroupsByName.dreamer, - debugInfo: defeatMapping, - previousPlayerPositionEvent: event.previousPlayerPositionEvent, - }); - } else { - assertNever(defeatMapping); - } - }); - } else if (isPlayerDataKilledField(field)) { - const enemyName = getEnemyNameFromKilledField(field); - const enemyInfo = enemies.byPlayerDataName[enemyName]; - if (enemyInfo && isEnemyBoss(enemyInfo)) { - recording.allPlayerDataEventsOfField(field).forEach((event) => { - if (event.value && !event.previousPlayerDataEventOfField?.value) { - splits.push( - createRecordingSplitFromEnemy( - event.msIntoGame, - enemyName, - enemyInfo, - event.previousPlayerPositionEvent, - ), - ); - } - }); - } - } else if (field === playerDataFields.byFieldName.greyPrinceDefeats) { - const enemyInfo = enemies.byPlayerDataName.GreyPrince; - recording.allPlayerDataEventsOfField(field).forEach((event) => { - if (event.value >= 1 && event.previousPlayerDataEventOfField?.value !== event.value) { - const enemyName = - greyPrinceNames.at(event.value >= greyPrinceNames.length ? -1 : event.value - 1) ?? - greyPrinceNames[0]!; - splits.push( - createRecordingSplitFromEnemy( - event.msIntoGame, - enemyName, - enemyInfo, - event.previousPlayerPositionEvent, - enemyName, - ), - ); - } - }); - } else if (isPlayerDataAbilityOrItemField(field)) { - const abilityOrItem = abilitiesAndItems[field.name]; - if (!abilityOrItem) continue; - recording.allPlayerDataEventsOfField(field).forEach((event) => { - const boolCondition = - isPlayerDataBoolField(event.field) && event.value && !event.previousPlayerDataEventOfField?.value; - const intCondition: boolean = - event.field.type === 'Int32' && - (event.value as any as number) > 0 && - event.previousPlayerDataEventOfField != null && - event.previousPlayerDataEventOfField.value < event.value; + if (defeatMapping === undefined) { + // ignore + // splits.push({ + // msIntoGame: event.msIntoGame, + // title: event.field.name, + // tooltip: 'Unknown', + // imageUrl: undefined, + // group: 'boss', + // debugInfo: undefined, + // }); + } else if ('enemy' in defeatMapping) { + const enemyInfo = enemies.byPlayerDataName[defeatMapping.enemy]; + splits.push( + createRecordingSplitFromEnemy( + event.msIntoGame, + enemyDefeatName, + enemyInfo, + event.previousPlayerPositionEvent, + defeatMapping.overrideName, + ), + ); + } else if ('dreamer' in defeatMapping) { + splits.push({ + msIntoGame: event.msIntoGame, + title: defeatMapping.dreamer, + tooltip: defeatMapping.dreamer + "'s Seal broken", + imageUrl: `/ingame-sprites/achievement/${defeatMapping.achievementSprite}.png`, + group: recordingSplitGroupsByName.dreamer, + debugInfo: defeatMapping, + previousPlayerPositionEvent: event.previousPlayerPositionEvent, + }); + } else { + assertNever(defeatMapping); + } + }); + } else if (isPlayerDataKilledField(field)) { + const enemyName = getEnemyNameFromKilledField(field); + const enemyInfo = enemies.byPlayerDataName[enemyName]; + if (enemyInfo && isEnemyBoss(enemyInfo)) { + recording.allPlayerDataEventsOfField(field).forEach((event) => { + if (event.value && !event.previousPlayerDataEventOfField?.value) { + const split = createRecordingSplitFromEnemy( + event.msIntoGame, + enemyName, + enemyInfo, + event.previousPlayerPositionEvent, + ); + splits.push(split); + console.log('kill split', split); + } + }); + } + } else if (field === playerDataFields.byFieldName.killsMegaBeamMiner) { + // our good friend the Crystal Guardian has a little special case + // its second form does not have a separate killed field, nor defeated field (afaik) + // so we need to check the kills field, which at first is 2 an is decreased with each kill + // since the killed field handles the first version, only the second version is interesting - if (boolCondition || intCondition) { - splits.push({ - msIntoGame: event.msIntoGame, - title: abilityOrItem.name, - tooltip: `Got ${abilityOrItem.name}`, - imageUrl: `/ingame-sprites/inventory/${abilityOrItem.spriteName}.png`, - group: - abilityOrItem.type === 'item' - ? recordingSplitGroupsByName.items - : abilityOrItem.type === 'ability' - ? recordingSplitGroupsByName.abilities - : assertNever(abilityOrItem.type), - debugInfo: event, - previousPlayerPositionEvent: event.previousPlayerPositionEvent, - }); - } - }); - } else if (field === playerDataFields.byFieldName.charmSlots) { - recording.allPlayerDataEventsOfField(field).forEach((event) => { - if ( - event.value > getDefaultValue(playerDataFields.byFieldName.charmSlots) && - event.previousPlayerDataEventOfField?.value !== event.value - ) { - splits.push({ - msIntoGame: event.msIntoGame, - title: `Charm Notch (nr ${event.value})`, - tooltip: `Got ${event.value} Charm Notches`, - imageUrl: '/ingame-sprites/inventory/Inv_0027_spell_slot.png', - group: recordingSplitGroupsByName.charmCollection, - debugInfo: event, - previousPlayerPositionEvent: event.previousPlayerPositionEvent, - }); - } - }); - } else if (field === playerDataFields.byFieldName.hasDreamNail) { - recording.allPlayerDataEventsOfField(field).forEach((event) => { - if (event.value && !event.previousPlayerDataEventOfField?.value) { - splits.push({ - msIntoGame: event.msIntoGame, - title: 'Dream Nail', - tooltip: 'Got the Dream Nail', - imageUrl: '/ingame-sprites/inventory/dream_nail_0003_1.png', - group: recordingSplitGroupsByName.abilities, - debugInfo: event, - previousPlayerPositionEvent: event.previousPlayerPositionEvent, - }); - } - }); - } else if (field === playerDataFields.byFieldName.hasDreamGate) { - recording.allPlayerDataEventsOfField(field).forEach((event) => { - if (event.value && !event.previousPlayerDataEventOfField?.value) { - splits.push({ - msIntoGame: event.msIntoGame, - title: 'Dream Gate', - tooltip: 'Got the Dream Gate', - imageUrl: '/ingame-sprites/inventory/dream_gate_inv_icon.png', - group: recordingSplitGroupsByName.abilities, - debugInfo: event, - previousPlayerPositionEvent: event.previousPlayerPositionEvent, - }); - } - }); - } else if (field === playerDataFields.byFieldName.dreamNailUpgraded) { - recording.allPlayerDataEventsOfField(field).forEach((event) => { - if (event.value && !event.previousPlayerDataEventOfField?.value) { - splits.push({ - msIntoGame: event.msIntoGame, - title: 'Awoken Dream Nail', - tooltip: 'Awoke the the Dream Nail', - imageUrl: '/ingame-sprites/inventory/dream_nail_0000_4.png', - group: recordingSplitGroupsByName.abilities, - debugInfo: event, - previousPlayerPositionEvent: event.previousPlayerPositionEvent, - }); - } - }); - } else if (field === playerDataFields.byFieldName.nailSmithUpgrades) { - recording.allPlayerDataEventsOfField(field).forEach((event) => { - if (event.value === 1 && event.previousPlayerDataEventOfField?.value !== 1) { - splits.push({ - msIntoGame: event.msIntoGame, - title: `Sharpened Nail`, - tooltip: `Upgraded Nail first time`, - imageUrl: '/ingame-sprites/inventory/nail_upgrade_0002_sharpened_nail.png', - group: recordingSplitGroupsByName.items, - debugInfo: event, - previousPlayerPositionEvent: event.previousPlayerPositionEvent, - }); - } else if (event.value === 2 && event.previousPlayerDataEventOfField?.value !== 2) { - splits.push({ - msIntoGame: event.msIntoGame, - title: `Channelled Nail`, - tooltip: `Upgraded Nail second time`, - imageUrl: '/ingame-sprites/inventory/nail_upgrade_0002_channel-nail.png', - group: recordingSplitGroupsByName.items, - debugInfo: event, - previousPlayerPositionEvent: event.previousPlayerPositionEvent, - }); - } else if (event.value === 3 && event.previousPlayerDataEventOfField?.value !== 3) { - splits.push({ - msIntoGame: event.msIntoGame, - title: `Coiled Nail`, - tooltip: `Upgraded Nail third time`, - imageUrl: '/ingame-sprites/inventory/nail_upgrade_03_coil_nail.png', - group: recordingSplitGroupsByName.items, - debugInfo: event, - previousPlayerPositionEvent: event.previousPlayerPositionEvent, - }); - } else if (event.value === 4 && event.previousPlayerDataEventOfField?.value !== 4) { - splits.push({ - msIntoGame: event.msIntoGame, - title: `Pure Nail`, - tooltip: `Upgraded Nail forth time`, - imageUrl: '/ingame-sprites/inventory/nail_upgrade_0000_pure-nail.png', - group: recordingSplitGroupsByName.items, - debugInfo: event, - previousPlayerPositionEvent: event.previousPlayerPositionEvent, - }); - } - }); - } else if (field === playerDataFields.byFieldName.heartPieces) { - recording.allPlayerDataEventsOfField(field).forEach((event) => { - if ( - event.value > 0 && - event.previousPlayerDataEventOfField && - event.value != event.previousPlayerDataEventOfField.value - ) { - // 0 filtered out, since the game sets the masks to 1,2,3 and once getting the 4th mask - // the value will quickly change to 4 and then set to 0 (if not the last mask shard). + recording.allPlayerDataEventsOfField(field).forEach((event) => { + const previous = event.previousPlayerDataEventOfField; + if (event.value === 0 && (!previous || previous.value > 0)) { + // the value is 0, so the second form is defeated + const enemyInfo = enemies.byPlayerDataName.MegaBeamMiner; + splits.push( + createRecordingSplitFromEnemy( + event.msIntoGame, + 'Enraged Guardian', + enemyInfo, + event.previousPlayerPositionEvent, + 'Enraged Guardian', + ), + ); + } + }); + } else if (field === playerDataFields.byFieldName.greyPrinceDefeats) { + const enemyInfo = enemies.byPlayerDataName.GreyPrince; + recording.allPlayerDataEventsOfField(field).forEach((event) => { + if (event.value >= 1 && event.previousPlayerDataEventOfField?.value !== event.value) { + const enemyName = + greyPrinceNames.at(event.value >= greyPrinceNames.length ? -1 : event.value - 1) ?? + greyPrinceNames[0]!; + splits.push( + createRecordingSplitFromEnemy( + event.msIntoGame, + enemyName, + enemyInfo, + event.previousPlayerPositionEvent, + enemyName, + ), + ); + } + }); + } else if (isPlayerDataAbilityOrItemField(field)) { + const abilityOrItem = abilitiesAndItems[field.name]; + if (!abilityOrItem) continue; + recording.allPlayerDataEventsOfField(field).forEach((event) => { + const boolCondition = + isPlayerDataBoolField(event.field) && event.value && !event.previousPlayerDataEventOfField?.value; + const intCondition: boolean = + event.field.type === 'Int32' && + (event.value as any as number) > 0 && + event.previousPlayerDataEventOfField != null && + event.previousPlayerDataEventOfField.value < event.value; - let image: string; + if (boolCondition || intCondition) { + splits.push({ + msIntoGame: event.msIntoGame, + title: abilityOrItem.name, + tooltip: `Got ${abilityOrItem.name}`, + imageUrl: `/ingame-sprites/inventory/${abilityOrItem.spriteName}.png`, + group: + abilityOrItem.type === 'item' + ? recordingSplitGroupsByName.items + : abilityOrItem.type === 'ability' + ? recordingSplitGroupsByName.abilities + : assertNever(abilityOrItem.type), + debugInfo: event, + previousPlayerPositionEvent: event.previousPlayerPositionEvent, + }); + } + }); + } else if (field === playerDataFields.byFieldName.charmSlots) { + recording.allPlayerDataEventsOfField(field).forEach((event) => { + if ( + event.value > getDefaultValue(playerDataFields.byFieldName.charmSlots) && + event.previousPlayerDataEventOfField?.value !== event.value + ) { + splits.push({ + msIntoGame: event.msIntoGame, + title: `Charm Notch (nr ${event.value})`, + tooltip: `Got ${event.value} Charm Notches`, + imageUrl: '/ingame-sprites/inventory/Inv_0027_spell_slot.png', + group: recordingSplitGroupsByName.charmCollection, + debugInfo: event, + previousPlayerPositionEvent: event.previousPlayerPositionEvent, + }); + } + }); + } else if (field === playerDataFields.byFieldName.hasDreamNail) { + recording.allPlayerDataEventsOfField(field).forEach((event) => { + if (event.value && !event.previousPlayerDataEventOfField?.value) { + splits.push({ + msIntoGame: event.msIntoGame, + title: 'Dream Nail', + tooltip: 'Got the Dream Nail', + imageUrl: '/ingame-sprites/inventory/dream_nail_0003_1.png', + group: recordingSplitGroupsByName.abilities, + debugInfo: event, + previousPlayerPositionEvent: event.previousPlayerPositionEvent, + }); + } + }); + } else if (field === playerDataFields.byFieldName.hasDreamGate) { + recording.allPlayerDataEventsOfField(field).forEach((event) => { + if (event.value && !event.previousPlayerDataEventOfField?.value) { + splits.push({ + msIntoGame: event.msIntoGame, + title: 'Dream Gate', + tooltip: 'Got the Dream Gate', + imageUrl: '/ingame-sprites/inventory/dream_gate_inv_icon.png', + group: recordingSplitGroupsByName.abilities, + debugInfo: event, + previousPlayerPositionEvent: event.previousPlayerPositionEvent, + }); + } + }); + } else if (field === playerDataFields.byFieldName.dreamNailUpgraded) { + recording.allPlayerDataEventsOfField(field).forEach((event) => { + if (event.value && !event.previousPlayerDataEventOfField?.value) { + splits.push({ + msIntoGame: event.msIntoGame, + title: 'Awoken Dream Nail', + tooltip: 'Awoke the the Dream Nail', + imageUrl: '/ingame-sprites/inventory/dream_nail_0000_4.png', + group: recordingSplitGroupsByName.abilities, + debugInfo: event, + previousPlayerPositionEvent: event.previousPlayerPositionEvent, + }); + } + }); + } else if (field === playerDataFields.byFieldName.nailSmithUpgrades) { + recording.allPlayerDataEventsOfField(field).forEach((event) => { + if (event.value === 1 && event.previousPlayerDataEventOfField?.value !== 1) { + splits.push({ + msIntoGame: event.msIntoGame, + title: `Sharpened Nail`, + tooltip: `Upgraded Nail first time`, + imageUrl: '/ingame-sprites/inventory/nail_upgrade_0002_sharpened_nail.png', + group: recordingSplitGroupsByName.items, + debugInfo: event, + previousPlayerPositionEvent: event.previousPlayerPositionEvent, + }); + } else if (event.value === 2 && event.previousPlayerDataEventOfField?.value !== 2) { + splits.push({ + msIntoGame: event.msIntoGame, + title: `Channelled Nail`, + tooltip: `Upgraded Nail second time`, + imageUrl: '/ingame-sprites/inventory/nail_upgrade_0002_channel-nail.png', + group: recordingSplitGroupsByName.items, + debugInfo: event, + previousPlayerPositionEvent: event.previousPlayerPositionEvent, + }); + } else if (event.value === 3 && event.previousPlayerDataEventOfField?.value !== 3) { + splits.push({ + msIntoGame: event.msIntoGame, + title: `Coiled Nail`, + tooltip: `Upgraded Nail third time`, + imageUrl: '/ingame-sprites/inventory/nail_upgrade_03_coil_nail.png', + group: recordingSplitGroupsByName.items, + debugInfo: event, + previousPlayerPositionEvent: event.previousPlayerPositionEvent, + }); + } else if (event.value === 4 && event.previousPlayerDataEventOfField?.value !== 4) { + splits.push({ + msIntoGame: event.msIntoGame, + title: `Pure Nail`, + tooltip: `Upgraded Nail forth time`, + imageUrl: '/ingame-sprites/inventory/nail_upgrade_0000_pure-nail.png', + group: recordingSplitGroupsByName.items, + debugInfo: event, + previousPlayerPositionEvent: event.previousPlayerPositionEvent, + }); + } + }); + } else if (field === playerDataFields.byFieldName.heartPieces) { + recording.allPlayerDataEventsOfField(field).forEach((event) => { + if ( + event.value > 0 && + event.previousPlayerDataEventOfField && + event.value != event.previousPlayerDataEventOfField.value + ) { + // 0 filtered out, since the game sets the masks to 1,2,3 and once getting the 4th mask + // the value will quickly change to 4 and then set to 0 (if not the last mask shard). - switch (event.value) { - case 1: - image = 'HP_UI_010007'; - break; - case 2: - image = 'HP_UI_020007'; - break; - case 3: - image = 'HP_UI_030007'; - break; - case 4: - image = 'HP_UI_040004'; - break; - default: - // should never happen in a unmodded game - image = 'HP_UI_010007'; - break; - } + let image: string; - splits.push({ - msIntoGame: event.msIntoGame, - title: `Mask Shard (${event.value}/4)`, - tooltip: `Got a Mask Shard (${event.value}/4)`, - imageUrl: `/ingame-sprites/inventory/${image}.png`, - group: recordingSplitGroupsByName.items, - debugInfo: event, - previousPlayerPositionEvent: event.previousPlayerPositionEvent, - }); - } - }); - } else if (field === playerDataFields.byFieldName.vesselFragments) { - recording.allPlayerDataEventsOfField(field).forEach((event) => { - if ( - event.value > 0 && - event.previousPlayerDataEventOfField && - event.value != event.previousPlayerDataEventOfField.value - ) { - // 0 filtered out, since the game sets the masks to 1,2,3 and once getting the 4th mask - // the value will quickly change to 4 and then set to 0 (if not the last mask shard). + switch (event.value) { + case 1: + image = 'HP_UI_010007'; + break; + case 2: + image = 'HP_UI_020007'; + break; + case 3: + image = 'HP_UI_030007'; + break; + case 4: + image = 'HP_UI_040004'; + break; + default: + // should never happen in a unmodded game + image = 'HP_UI_010007'; + break; + } - let image: string; + splits.push({ + msIntoGame: event.msIntoGame, + title: `Mask Shard (${event.value}/4)`, + tooltip: `Got a Mask Shard (${event.value}/4)`, + imageUrl: `/ingame-sprites/inventory/${image}.png`, + group: recordingSplitGroupsByName.items, + debugInfo: event, + previousPlayerPositionEvent: event.previousPlayerPositionEvent, + }); + } + }); + } else if (field === playerDataFields.byFieldName.vesselFragments) { + recording.allPlayerDataEventsOfField(field).forEach((event) => { + if ( + event.value > 0 && + event.previousPlayerDataEventOfField && + event.value != event.previousPlayerDataEventOfField.value + ) { + // 0 filtered out, since the game sets the masks to 1,2,3 and once getting the 4th mask + // the value will quickly change to 4 and then set to 0 (if not the last mask shard). - switch (event.value) { - case 1: - image = 'Inventory_soul_vessel_level_01'; - break; - case 2: - image = 'Inventory_soul_vessel'; - break; - case 3: - image = 'Inventory_soul_vessel_full'; - break; - default: - // should never happen in a unmodded game - image = 'Inventory_soul_vessel_level_01'; - break; - } + let image: string; - splits.push({ - msIntoGame: event.msIntoGame, - title: `Vessel Fragment (${event.value}/3)`, - tooltip: `Got a Vessel Fragment (${event.value}/3)`, - imageUrl: `/ingame-sprites/inventory/${image}.png`, - group: recordingSplitGroupsByName.items, - debugInfo: event, - previousPlayerPositionEvent: event.previousPlayerPositionEvent, - }); - } - }); - } else { - [ - { field: playerDataFields.byFieldName.mapAbyss, title: 'Abyss Map' }, - { - field: playerDataFields.byFieldName.mapCity, - title: 'City of Tears Map', - }, - { field: playerDataFields.byFieldName.mapCliffs, title: 'Howling Cliffs Map' }, - { - field: playerDataFields.byFieldName.mapCrossroads, - title: 'Forgotten Crossroads Map', - }, - { field: playerDataFields.byFieldName.mapDeepnest, title: 'Deepnest Map' }, - { - field: playerDataFields.byFieldName.mapFogCanyon, - title: 'Fog Canyon Map', - }, - { field: playerDataFields.byFieldName.mapGreenpath, title: 'Greenpath Map' }, - { - field: playerDataFields.byFieldName.mapMines, - title: 'Crystal Peak Map', - }, - { field: playerDataFields.byFieldName.mapOutskirts, title: "Kingdom's Edge Map" }, - { - field: playerDataFields.byFieldName.mapRestingGrounds, - title: 'Resting Grounds Map', - }, - { field: playerDataFields.byFieldName.mapRoyalGardens, title: "Queen's Gardens Map" }, + switch (event.value) { + case 1: + image = 'Inventory_soul_vessel_level_01'; + break; + case 2: + image = 'Inventory_soul_vessel'; + break; + case 3: + image = 'Inventory_soul_vessel_full'; + break; + default: + // should never happen in a unmodded game + image = 'Inventory_soul_vessel_level_01'; + break; + } - { - field: playerDataFields.byFieldName.mapFungalWastes, - title: 'Fungal Wastes Map', - }, - { field: playerDataFields.byFieldName.mapWaterways, title: 'Royal Waterways Map' }, - ].map((map) => { - if (field === map.field) { - recording.allPlayerDataEventsOfField(field).forEach((event) => { - if (event.value && !event.previousPlayerDataEventOfField?.value) { - splits.push({ - msIntoGame: event.msIntoGame, - title: map.title, - tooltip: 'Got ' + map.title, - imageUrl: '/ingame-sprites/inventory/inv_item__0008_jar_col_map.png', - group: recordingSplitGroupsByName.items, - debugInfo: event, - previousPlayerPositionEvent: event.previousPlayerPositionEvent, - }); - } - }); - } - }); - } - } + splits.push({ + msIntoGame: event.msIntoGame, + title: `Vessel Fragment (${event.value}/3)`, + tooltip: `Got a Vessel Fragment (${event.value}/3)`, + imageUrl: `/ingame-sprites/inventory/${image}.png`, + group: recordingSplitGroupsByName.items, + debugInfo: event, + previousPlayerPositionEvent: event.previousPlayerPositionEvent, + }); + } + }); + } else { + [ + { field: playerDataFields.byFieldName.mapAbyss, title: 'Abyss Map' }, + { + field: playerDataFields.byFieldName.mapCity, + title: 'City of Tears Map', + }, + { field: playerDataFields.byFieldName.mapCliffs, title: 'Howling Cliffs Map' }, + { + field: playerDataFields.byFieldName.mapCrossroads, + title: 'Forgotten Crossroads Map', + }, + { field: playerDataFields.byFieldName.mapDeepnest, title: 'Deepnest Map' }, + { + field: playerDataFields.byFieldName.mapFogCanyon, + title: 'Fog Canyon Map', + }, + { field: playerDataFields.byFieldName.mapGreenpath, title: 'Greenpath Map' }, + { + field: playerDataFields.byFieldName.mapMines, + title: 'Crystal Peak Map', + }, + { field: playerDataFields.byFieldName.mapOutskirts, title: "Kingdom's Edge Map" }, + { + field: playerDataFields.byFieldName.mapRestingGrounds, + title: 'Resting Grounds Map', + }, + { field: playerDataFields.byFieldName.mapRoyalGardens, title: "Queen's Gardens Map" }, - // ----- CHARMS ----- - for (const virtualCharm of virtualCharms) { - for (const frameEndEvent of recording.frameEndEvents) { - if ( - virtualCharm.hasCharm(frameEndEvent) && - (!frameEndEvent.previousFrameEndEvent || !virtualCharm.hasCharm(frameEndEvent.previousFrameEndEvent)) - ) { - splits.push({ - msIntoGame: frameEndEvent.msIntoGame, - title: `${virtualCharm.name}`, - tooltip: `Got ${virtualCharm.name}`, - imageUrl: `/ingame-sprites/charms/${virtualCharm.spriteName}.png`, - group: recordingSplitGroupsByName.charmCollection, - debugInfo: undefined, - previousPlayerPositionEvent: frameEndEvent.previousPlayerPositionEvent, - }); - } - } - } + { + field: playerDataFields.byFieldName.mapFungalWastes, + title: 'Fungal Wastes Map', + }, + { field: playerDataFields.byFieldName.mapWaterways, title: 'Royal Waterways Map' }, + ].map((map) => { + if (field === map.field) { + recording.allPlayerDataEventsOfField(field).forEach((event) => { + if (event.value && !event.previousPlayerDataEventOfField?.value) { + splits.push({ + msIntoGame: event.msIntoGame, + title: map.title, + tooltip: 'Got ' + map.title, + imageUrl: '/ingame-sprites/inventory/inv_item__0008_jar_col_map.png', + group: recordingSplitGroupsByName.items, + debugInfo: event, + previousPlayerPositionEvent: event.previousPlayerPositionEvent, + }); + } + }); + } + }); + } + } - // ----- FLOWER ----- - let hadFlowerLastFrame = false; - let hadBrokenFlowerLastFrame = false; - for (const frameEndEvent of recording.frameEndEvents) { - const xunFlowerBrokenThisFrame = frameEndEvent.xunFlowerBroken; - const hasFlowerThisFrame = frameEndEvent.hasXunFlower && !xunFlowerBrokenThisFrame; + // ----- CHARMS ----- + for (const virtualCharm of virtualCharms) { + for (const frameEndEvent of recording.frameEndEvents) { + if ( + virtualCharm.hasCharm(frameEndEvent) && + (!frameEndEvent.previousFrameEndEvent || !virtualCharm.hasCharm(frameEndEvent.previousFrameEndEvent)) + ) { + splits.push({ + msIntoGame: frameEndEvent.msIntoGame, + title: `${virtualCharm.name}`, + tooltip: `Got ${virtualCharm.name}`, + imageUrl: `/ingame-sprites/charms/${virtualCharm.spriteName}.png`, + group: recordingSplitGroupsByName.charmCollection, + debugInfo: undefined, + previousPlayerPositionEvent: frameEndEvent.previousPlayerPositionEvent, + }); + } + } + } - if (!hadFlowerLastFrame && hasFlowerThisFrame) { - splits.push({ - msIntoGame: frameEndEvent.msIntoGame, - title: 'Delicate Flower', - tooltip: 'Got Delicate Flower', - imageUrl: '/ingame-sprites/inventory/White_Flower_Full.png', - group: recordingSplitGroupsByName.items, - debugInfo: undefined, - previousPlayerPositionEvent: frameEndEvent.previousPlayerPositionEvent, - }); - } - if (!hadBrokenFlowerLastFrame && xunFlowerBrokenThisFrame) { - splits.push({ - msIntoGame: frameEndEvent.msIntoGame, - title: 'Ruined Flower', - tooltip: 'Broke Delicate Flower', - imageUrl: '/ingame-sprites/inventory/White_Flower_Half.png', - group: recordingSplitGroupsByName.items, - debugInfo: undefined, - previousPlayerPositionEvent: frameEndEvent.previousPlayerPositionEvent, - }); - } - hadFlowerLastFrame = hasFlowerThisFrame; - hadBrokenFlowerLastFrame = xunFlowerBrokenThisFrame; - } + // ----- FLOWER ----- + let hadFlowerLastFrame = false; + let hadBrokenFlowerLastFrame = false; + for (const frameEndEvent of recording.frameEndEvents) { + const xunFlowerBrokenThisFrame = frameEndEvent.xunFlowerBroken; + const hasFlowerThisFrame = frameEndEvent.hasXunFlower && !xunFlowerBrokenThisFrame; - // Spell levels - for (const frameEndEvent of recording.frameEndEvents) { - // fireball - if (frameEndEvent.fireballLevel === 1 && frameEndEvent.previousFrameEndEvent?.fireballLevel !== 1) { - splits.push({ - msIntoGame: frameEndEvent.msIntoGame, - title: 'Vengeful Spirit', - tooltip: 'Got Vengeful Spirit', - imageUrl: '/ingame-sprites/inventory/Inv_0025_spell_fireball_01.png', - group: recordingSplitGroupsByName.abilities, - debugInfo: undefined, - previousPlayerPositionEvent: frameEndEvent.previousPlayerPositionEvent, - }); - } - if (frameEndEvent.fireballLevel === 2 && frameEndEvent.previousFrameEndEvent?.fireballLevel !== 2) { - splits.push({ - msIntoGame: frameEndEvent.msIntoGame, - title: 'Shade Soul', - tooltip: 'Got Shade Soul (upgrade for Vengeful Spirit)', - imageUrl: '/ingame-sprites/inventory/Inv_0025_spell_fireball_01_level2.png', - group: recordingSplitGroupsByName.abilities, - debugInfo: undefined, - previousPlayerPositionEvent: frameEndEvent.previousPlayerPositionEvent, - }); - } - // up spell - if (frameEndEvent.screamLevel === 1 && frameEndEvent.previousFrameEndEvent?.screamLevel !== 1) { - splits.push({ - msIntoGame: frameEndEvent.msIntoGame, - title: 'Howling Wraiths', - tooltip: 'Got Howling Wraiths', - imageUrl: '/ingame-sprites/inventory/Inv_0024_spell_scream_01.png', - group: recordingSplitGroupsByName.abilities, - debugInfo: undefined, - previousPlayerPositionEvent: frameEndEvent.previousPlayerPositionEvent, - }); - } - if (frameEndEvent.screamLevel === 2 && frameEndEvent.previousFrameEndEvent?.screamLevel !== 2) { - splits.push({ - msIntoGame: frameEndEvent.msIntoGame, - title: 'Abyss Shriek', - tooltip: 'Got Abyss Shriek (upgrade for Howling Wraiths)', - imageUrl: '/ingame-sprites/inventory/Inv_0024_spell_scream_01_level2.png', - group: recordingSplitGroupsByName.abilities, - debugInfo: undefined, - previousPlayerPositionEvent: frameEndEvent.previousPlayerPositionEvent, - }); - } - // down spell - if (frameEndEvent.quakeLevel === 1 && frameEndEvent.previousFrameEndEvent?.quakeLevel !== 1) { - splits.push({ - msIntoGame: frameEndEvent.msIntoGame, - title: 'Desolate Dive', - tooltip: 'Got Howling Wraiths', - imageUrl: '/ingame-sprites/inventory/Inv_0026_spell_quake_01.png', - group: recordingSplitGroupsByName.abilities, - debugInfo: undefined, - previousPlayerPositionEvent: frameEndEvent.previousPlayerPositionEvent, - }); - } - if (frameEndEvent.quakeLevel === 2 && frameEndEvent.previousFrameEndEvent?.quakeLevel !== 2) { - splits.push({ - msIntoGame: frameEndEvent.msIntoGame, - title: 'Descending Dark', - tooltip: 'Got Descending Dark (upgrade for Desolate Dive)', - imageUrl: '/ingame-sprites/inventory/Inv_0026_spell_quake_01_level2.png', - group: recordingSplitGroupsByName.abilities, - debugInfo: undefined, - previousPlayerPositionEvent: frameEndEvent.previousPlayerPositionEvent, - }); - } - } + if (!hadFlowerLastFrame && hasFlowerThisFrame) { + splits.push({ + msIntoGame: frameEndEvent.msIntoGame, + title: 'Delicate Flower', + tooltip: 'Got Delicate Flower', + imageUrl: '/ingame-sprites/inventory/White_Flower_Full.png', + group: recordingSplitGroupsByName.items, + debugInfo: undefined, + previousPlayerPositionEvent: frameEndEvent.previousPlayerPositionEvent, + }); + } + if (!hadBrokenFlowerLastFrame && xunFlowerBrokenThisFrame) { + splits.push({ + msIntoGame: frameEndEvent.msIntoGame, + title: 'Ruined Flower', + tooltip: 'Broke Delicate Flower', + imageUrl: '/ingame-sprites/inventory/White_Flower_Half.png', + group: recordingSplitGroupsByName.items, + debugInfo: undefined, + previousPlayerPositionEvent: frameEndEvent.previousPlayerPositionEvent, + }); + } + hadFlowerLastFrame = hasFlowerThisFrame; + hadBrokenFlowerLastFrame = xunFlowerBrokenThisFrame; + } - return splits.sort((a, b) => a.msIntoGame - b.msIntoGame); + // Spell levels + for (const frameEndEvent of recording.frameEndEvents) { + // fireball + if (frameEndEvent.fireballLevel === 1 && frameEndEvent.previousFrameEndEvent?.fireballLevel !== 1) { + splits.push({ + msIntoGame: frameEndEvent.msIntoGame, + title: 'Vengeful Spirit', + tooltip: 'Got Vengeful Spirit', + imageUrl: '/ingame-sprites/inventory/Inv_0025_spell_fireball_01.png', + group: recordingSplitGroupsByName.abilities, + debugInfo: undefined, + previousPlayerPositionEvent: frameEndEvent.previousPlayerPositionEvent, + }); + } + if (frameEndEvent.fireballLevel === 2 && frameEndEvent.previousFrameEndEvent?.fireballLevel !== 2) { + splits.push({ + msIntoGame: frameEndEvent.msIntoGame, + title: 'Shade Soul', + tooltip: 'Got Shade Soul (upgrade for Vengeful Spirit)', + imageUrl: '/ingame-sprites/inventory/Inv_0025_spell_fireball_01_level2.png', + group: recordingSplitGroupsByName.abilities, + debugInfo: undefined, + previousPlayerPositionEvent: frameEndEvent.previousPlayerPositionEvent, + }); + } + // up spell + if (frameEndEvent.screamLevel === 1 && frameEndEvent.previousFrameEndEvent?.screamLevel !== 1) { + splits.push({ + msIntoGame: frameEndEvent.msIntoGame, + title: 'Howling Wraiths', + tooltip: 'Got Howling Wraiths', + imageUrl: '/ingame-sprites/inventory/Inv_0024_spell_scream_01.png', + group: recordingSplitGroupsByName.abilities, + debugInfo: undefined, + previousPlayerPositionEvent: frameEndEvent.previousPlayerPositionEvent, + }); + } + if (frameEndEvent.screamLevel === 2 && frameEndEvent.previousFrameEndEvent?.screamLevel !== 2) { + splits.push({ + msIntoGame: frameEndEvent.msIntoGame, + title: 'Abyss Shriek', + tooltip: 'Got Abyss Shriek (upgrade for Howling Wraiths)', + imageUrl: '/ingame-sprites/inventory/Inv_0024_spell_scream_01_level2.png', + group: recordingSplitGroupsByName.abilities, + debugInfo: undefined, + previousPlayerPositionEvent: frameEndEvent.previousPlayerPositionEvent, + }); + } + // down spell + if (frameEndEvent.quakeLevel === 1 && frameEndEvent.previousFrameEndEvent?.quakeLevel !== 1) { + splits.push({ + msIntoGame: frameEndEvent.msIntoGame, + title: 'Desolate Dive', + tooltip: 'Got Howling Wraiths', + imageUrl: '/ingame-sprites/inventory/Inv_0026_spell_quake_01.png', + group: recordingSplitGroupsByName.abilities, + debugInfo: undefined, + previousPlayerPositionEvent: frameEndEvent.previousPlayerPositionEvent, + }); + } + if (frameEndEvent.quakeLevel === 2 && frameEndEvent.previousFrameEndEvent?.quakeLevel !== 2) { + splits.push({ + msIntoGame: frameEndEvent.msIntoGame, + title: 'Descending Dark', + tooltip: 'Got Descending Dark (upgrade for Desolate Dive)', + imageUrl: '/ingame-sprites/inventory/Inv_0026_spell_quake_01_level2.png', + group: recordingSplitGroupsByName.abilities, + debugInfo: undefined, + previousPlayerPositionEvent: frameEndEvent.previousPlayerPositionEvent, + }); + } + } + + return splits.sort((a, b) => a.msIntoGame - b.msIntoGame); } diff --git a/src/lib/viz/store/store-context.tsx b/src/lib/viz/store/store-context.tsx index 676f6d00..3c8814c6 100644 --- a/src/lib/viz/store/store-context.tsx +++ b/src/lib/viz/store/store-context.tsx @@ -1,4 +1,4 @@ -import { JSXElement } from 'solid-js'; +import { createEffect, JSXElement, onCleanup } from 'solid-js'; import { AggregationStoreContext, createAggregationStore } from './aggregation-store'; import { AnimationStoreContext, createAnimationStore } from './animation-store'; import { createExtraChartStore, ExtraChartStoreContext } from './extra-chart-store'; @@ -51,6 +51,31 @@ export function RunStoresProvider(props: { children: JSXElement }) { mapZoomStore, ); + createEffect(() => { + const allStores = { + gameplayStore, + hoverMsStore, + animationStore, + mapZoomStore, + traceStore, + extraChartStore, + playerDataAnimationStore, + roomDisplayStore, + aggregationStore, + roomColoringStore, + splitsStore, + tourStore, + }; + + (window as any).stores = allStores; + + onCleanup(() => { + if ((window as any).stores === allStores) { + (window as any).stores = null; + } + }); + }); + return ( diff --git a/src/server/ingameauth/allow-login.ts b/src/server/ingameauth/allow-login.ts index eb58fe6c..02708c8d 100644 --- a/src/server/ingameauth/allow-login.ts +++ b/src/server/ingameauth/allow-login.ts @@ -1,4 +1,3 @@ -import * as v from 'valibot'; import { ingameAuth } from '~/server/db/schema'; import { action } from '@solidjs/router'; @@ -7,9 +6,9 @@ import { getUserOrThrow } from '~/lib/auth/shared'; import { COOKIE_INGAME_AUTH_URL_ID } from '~/lib/cookies/cookie-names'; import { jsonWithCookies } from '~/lib/cookies/cookies-response-helpers'; import { serverCookiesGet } from '~/lib/cookies/cookies-server'; +import { raise } from '~/lib/parser'; import { db } from '~/server/db'; import { isMax10MinutesOld } from './utils'; -import { raise } from '~/lib/parser'; export const ingameAuthAllowLogin = action(async () => { 'use server';