zenno/career/ismdata: start visualization
This commit is contained in:
@@ -13,6 +13,8 @@
|
|||||||
import SupportIcon from '$lib/SupportIcon.svelte';
|
import SupportIcon from '$lib/SupportIcon.svelte';
|
||||||
import SkillIcon from '$lib/SkillIcon.svelte';
|
import SkillIcon from '$lib/SkillIcon.svelte';
|
||||||
import Spark from '$lib/Spark.svelte';
|
import Spark from '$lib/Spark.svelte';
|
||||||
|
import SupportCardGainChart from './SupportCardGainChart.svelte';
|
||||||
|
import { DEPENDENT, INDEPENDENT, type SupportCardDatum } from './SupportCardGainChart';
|
||||||
|
|
||||||
let characters = new SvelteMap<number, character.Character>();
|
let characters = new SvelteMap<number, character.Character>();
|
||||||
let skills = new SvelteMap<number, skill.Skill>();
|
let skills = new SvelteMap<number, skill.Skill>();
|
||||||
@@ -55,6 +57,7 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
let files: FileList | undefined = $state();
|
let files: FileList | undefined = $state();
|
||||||
|
let careersOpen = $state(false);
|
||||||
const parses = $derived.by(async () => {
|
const parses = $derived.by(async () => {
|
||||||
if (files == null) {
|
if (files == null) {
|
||||||
return [];
|
return [];
|
||||||
@@ -75,7 +78,11 @@
|
|||||||
let exclude = new SvelteSet<string>();
|
let exclude = new SvelteSet<string>();
|
||||||
const errors = $derived((await parses).filter((f) => 'err' in f) as Array<{ f: File; err: unknown }>);
|
const errors = $derived((await parses).filter((f) => 'err' in f) as Array<{ f: File; err: unknown }>);
|
||||||
const loaded = $derived((await parses).filter((f) => 'ok' in f) as Array<{ f: File; ok: ism.Career }>);
|
const loaded = $derived((await parses).filter((f) => 'ok' in f) as Array<{ f: File; ok: ism.Career }>);
|
||||||
const careers = $derived(loaded.filter((f) => !exclude.has(f.f.name)));
|
const careers = $derived(loaded.filter((f) => !exclude.has(f.f.name)).map((f) => f.ok));
|
||||||
|
|
||||||
|
let supportGainX: keyof SupportCardDatum = $state('Friendship Bonus');
|
||||||
|
let supportGainY: keyof SupportCardDatum = $state('Total Stats');
|
||||||
|
let supportGainS: keyof SupportCardDatum = $state('Source');
|
||||||
|
|
||||||
function groupskill(g: skill.SkillGroup | undefined, rarity: ism.SkillTip['rarity']) {
|
function groupskill(g: skill.SkillGroup | undefined, rarity: ism.SkillTip['rarity']) {
|
||||||
if (g == null) {
|
if (g == null) {
|
||||||
@@ -116,13 +123,14 @@
|
|||||||
|
|
||||||
{#if loaded.length > 0}
|
{#if loaded.length > 0}
|
||||||
<div class="mx-auto my-8 max-w-6xl rounded-md border p-4">
|
<div class="mx-auto my-8 max-w-6xl rounded-md border p-4">
|
||||||
<details class="w-full">
|
<details class="w-full" bind:open={careersOpen}>
|
||||||
<summary>
|
<summary>
|
||||||
{(files?.length ?? 0) - errors.length} careers
|
{(files?.length ?? 0) - errors.length} careers
|
||||||
{#if exclude.size !== 0}
|
{#if exclude.size !== 0}
|
||||||
({loaded.length - careers.length} excluded)
|
({loaded.length - careers.length} excluded)
|
||||||
{/if}
|
{/if}
|
||||||
</summary>
|
</summary>
|
||||||
|
{#if careersOpen}
|
||||||
{#each loaded as career (career.ok.end_info.chara_info.single_mode_chara_id)}
|
{#each loaded as career (career.ok.end_info.chara_info.single_mode_chara_id)}
|
||||||
{@const uma = umas.get(career.ok.end_info.chara_info.card_id)}
|
{@const uma = umas.get(career.ok.end_info.chara_info.card_id)}
|
||||||
{@const scen = scenarios.get(career.ok.end_info.chara_info.scenario_id)}
|
{@const scen = scenarios.get(career.ok.end_info.chara_info.scenario_id)}
|
||||||
@@ -159,6 +167,24 @@
|
|||||||
{/snippet}
|
{/snippet}
|
||||||
</CareerOverview>
|
</CareerOverview>
|
||||||
{/each}
|
{/each}
|
||||||
|
{/if}
|
||||||
</details>
|
</details>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="flex">
|
||||||
|
<select class="flex-1" bind:value={supportGainY}>
|
||||||
|
{#each DEPENDENT as t (t.name)}
|
||||||
|
<option value={t.name}>{t.name}</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
<span class="mx-2 flex-none">vs</span>
|
||||||
|
<select class="flex-1" bind:value={supportGainX}>
|
||||||
|
{#each INDEPENDENT as t (t.name)}
|
||||||
|
<option value={t.name}>{t.name}</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="mx-auto mb-8 h-96 w-4xl">
|
||||||
|
<SupportCardGainChart {careers} {supports} {scenarios} x={supportGainX} y={supportGainY} symbol={supportGainS} />
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -19,6 +19,8 @@
|
|||||||
const totalSparks = $derived(
|
const totalSparks = $derived(
|
||||||
career.progress_log_info.succession_factor_gain_array.reduce((a, y) => a + y.gain_factor_info_array.length, 0),
|
career.progress_log_info.succession_factor_gain_array.reduce((a, y) => a + y.gain_factor_info_array.length, 0),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let open = $state(false);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#snippet gainStat(name: ComponentProps<typeof SpecialtyIcon>['name'], stat: number, max: number)}
|
{#snippet gainStat(name: ComponentProps<typeof SpecialtyIcon>['name'], stat: number, max: number)}
|
||||||
@@ -67,8 +69,9 @@
|
|||||||
{@render support(card)}
|
{@render support(card)}
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
<details class="mx-2 mb-1 grid w-full grid-cols-1 gap-x-2">
|
<details class="mx-2 mb-1 grid w-full grid-cols-1 gap-x-2" bind:open>
|
||||||
<summary>Gains per source</summary>
|
<summary>Gains per source</summary>
|
||||||
|
{#if open}
|
||||||
{#each career.progress_log_info.support_card_gain_info_array as card (card.support_card_id)}
|
{#each career.progress_log_info.support_card_gain_info_array as card (card.support_card_id)}
|
||||||
{@const cardInfo = career.end_info.chara_info.support_card_array.find((c) => c.support_card_id === card.support_card_id)!}
|
{@const cardInfo = career.end_info.chara_info.support_card_array.find((c) => c.support_card_id === card.support_card_id)!}
|
||||||
<div class="flex w-full flex-row pr-4">
|
<div class="flex w-full flex-row pr-4">
|
||||||
@@ -92,6 +95,7 @@
|
|||||||
{@render gainInfo(career.progress_log_info.succession_gain_info)}
|
{@render gainInfo(career.progress_log_info.succession_gain_info)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{/if}
|
||||||
</details>
|
</details>
|
||||||
<details class="mx-2 mb-1 w-full pr-4">
|
<details class="mx-2 mb-1 w-full pr-4">
|
||||||
<summary>Hints for {career.end_info.chara_info.skill_tips_array.length} skills</summary>
|
<summary>Hints for {career.end_info.chara_info.skill_tips_array.length} skills</summary>
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import * as Plot from '@observablehq/plot';
|
||||||
|
import * as ism from '$lib/ism';
|
||||||
|
import * as support from '$lib/data/support';
|
||||||
|
import * as scenario from '$lib/data/scenario';
|
||||||
|
import type { SvelteMap } from 'svelte/reactivity';
|
||||||
|
import { supportVariables, type SupportCardDatum } from './SupportCardGainChart';
|
||||||
|
import type { Attachment } from 'svelte/attachments';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
careers: ism.Career[];
|
||||||
|
supports: SvelteMap<number, support.SupportCard>;
|
||||||
|
scenarios: SvelteMap<number, scenario.Scenario>;
|
||||||
|
x: keyof SupportCardDatum;
|
||||||
|
y: keyof SupportCardDatum;
|
||||||
|
symbol?: keyof SupportCardDatum;
|
||||||
|
}
|
||||||
|
|
||||||
|
let { careers, supports, scenarios, x, y, symbol = 'Scenario' }: Props = $props();
|
||||||
|
|
||||||
|
let width = $state(0);
|
||||||
|
let height = $state(0);
|
||||||
|
|
||||||
|
const data = $derived(
|
||||||
|
careers.flatMap((c) =>
|
||||||
|
supportVariables(
|
||||||
|
c,
|
||||||
|
(id: number) => supports.get(id)!,
|
||||||
|
(id: number) => scenarios.get(id)!,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const makeChart: Attachment = (el) => {
|
||||||
|
$effect(() => {
|
||||||
|
el?.firstChild?.remove();
|
||||||
|
el?.append(
|
||||||
|
Plot.plot({
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
x: {},
|
||||||
|
y: {},
|
||||||
|
color: { scheme: 'Magma' },
|
||||||
|
marks: [
|
||||||
|
Plot.frame(),
|
||||||
|
Plot.rect(data, Plot.bin({ fill: 'count', filter: null }, { x, y, interval: 1 })),
|
||||||
|
Plot.dot(data, { x, y, symbol }),
|
||||||
|
data.length > 0 ? Plot.tip(data, Plot.pointer({ x, y, symbol, stroke: symbol, className: 'plot-tip' })) : null,
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div bind:clientWidth={width} bind:clientHeight={height} class="h-full w-full">
|
||||||
|
<div role="img" {@attach makeChart}>
|
||||||
|
<span>the chart seems to have didn't</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
:global(.plot-tip) {
|
||||||
|
--plot-background: light-dark(var(--color-mist-200), var(--color-mist-800));
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,147 @@
|
|||||||
|
import * as ism from '$lib/ism';
|
||||||
|
import * as support from '$lib/data/support';
|
||||||
|
import * as scenario from '$lib/data/scenario';
|
||||||
|
|
||||||
|
function gainTotalStats(gain: ism.GainInfo) {
|
||||||
|
return gain.speed.value + gain.stamina.value + gain.power.value + gain.guts.value + gain.wiz.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DEPENDENT = [
|
||||||
|
{
|
||||||
|
name: 'Total Stats',
|
||||||
|
map: (gain: ism.GainInfo, _effects: support.Effect[]) => gainTotalStats(gain),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Total Stats + SP',
|
||||||
|
map: (gain: ism.GainInfo, _effects: support.Effect[]) => gainTotalStats(gain) + gain.skill_point,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Speed',
|
||||||
|
map: (gain: ism.GainInfo, _effects: support.Effect[]) => gain.speed.value,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Stamina',
|
||||||
|
map: (gain: ism.GainInfo, _effects: support.Effect[]) => gain.stamina.value,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Power',
|
||||||
|
map: (gain: ism.GainInfo, _effects: support.Effect[]) => gain.power.value,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Guts',
|
||||||
|
map: (gain: ism.GainInfo, _effects: support.Effect[]) => gain.guts.value,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Wit',
|
||||||
|
map: (gain: ism.GainInfo, _effects: support.Effect[]) => gain.wiz.value,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'SP',
|
||||||
|
map: (gain: ism.GainInfo, _effects: support.Effect[]) => gain.skill_point,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Skill Hints',
|
||||||
|
map: (gain: ism.GainInfo, effects: support.Effect[]) => {
|
||||||
|
// NOTE(zephyr): reduction starts with 1 because we want the value
|
||||||
|
// of each hint, not just the bonus from effects
|
||||||
|
const hintLevels = effects.reduce((l, e) => e.type === support.EffectType.HintLevels ? l + e.value : l, 1);
|
||||||
|
return gain.skill_tips_array.reduce((n, t) => n + Math.ceil(t.level / hintLevels), 0);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Skills',
|
||||||
|
map: (gain: ism.GainInfo, _effects: support.Effect[]) => gain.skill_tips_array.length,
|
||||||
|
},
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
function label(c: ism.Career): string {
|
||||||
|
return `${c.end_info.chara_info.single_mode_chara_id} - ${c.end_info.chara_info.start_time}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const INDEPENDENT = [
|
||||||
|
{
|
||||||
|
name: 'Friendship Bonus',
|
||||||
|
effect: support.EffectType.FriendshipBonus,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Mood Effect',
|
||||||
|
effect: support.EffectType.MoodEffect,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Speed Bonus',
|
||||||
|
effect: support.EffectType.SpeedBonus,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Stamina Bonus',
|
||||||
|
effect: support.EffectType.StaminaBonus,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Power Bonus',
|
||||||
|
effect: support.EffectType.PowerBonus,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Guts Bonus',
|
||||||
|
effect: support.EffectType.GutsBonus,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Wit Bonus',
|
||||||
|
effect: support.EffectType.WitBonus,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Skill Point Bonus',
|
||||||
|
effect: support.EffectType.SkillPointBonus,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Training Effectiveness',
|
||||||
|
effect: support.EffectType.TrainingEffectiveness,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Initial Friendship',
|
||||||
|
effect: support.EffectType.InitialFriendship,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Race Bonus',
|
||||||
|
effect: support.EffectType.RaceBonus,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Hint Frequency',
|
||||||
|
effect: support.EffectType.HintFrequency,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Specialty Priority',
|
||||||
|
effect: support.EffectType.SpecialtyPriority,
|
||||||
|
},
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export type SupportCardDatum = {
|
||||||
|
'Career': string;
|
||||||
|
'Scenario': string;
|
||||||
|
'Source': string;
|
||||||
|
'Unique Effect': string;
|
||||||
|
'Idle Mode Sub Rate': number;
|
||||||
|
'Races': number;
|
||||||
|
'Race Wins': number;
|
||||||
|
} & Record<typeof INDEPENDENT[number]['name'], number> & Record<typeof DEPENDENT[number]['name'], number>;
|
||||||
|
|
||||||
|
export function supportVariables(career: ism.Career, lookupCard: (id: number) => support.SupportCard, lookupScenario: (id: number) => scenario.Scenario): SupportCardDatum[] {
|
||||||
|
const deck = career.progress_log_info.support_card_gain_info_array.map((g) => lookupCard(g.support_card_id));
|
||||||
|
const scen = lookupScenario(career.end_info.chara_info.scenario_id);
|
||||||
|
return career.progress_log_info.support_card_gain_info_array.map((g, i) => {
|
||||||
|
const card = deck[i];
|
||||||
|
const level = support.level(card.rarity, career.end_info.chara_info.support_card_array[i].exp);
|
||||||
|
const { effects, unique } = support.effects(lookupCard(g.support_card_id), level);
|
||||||
|
const indep = Object.fromEntries(INDEPENDENT.map((t) => [t.name, effects.find((e) => e.type === t.effect)?.value ?? 0])) as Record<typeof INDEPENDENT[number]['name'], number>;
|
||||||
|
const dep = Object.fromEntries(DEPENDENT.map((t) => [t.name, t.map(g.gain_info, effects)])) as Record<typeof DEPENDENT[number]['name'], number>;
|
||||||
|
return {
|
||||||
|
'Career': label(career),
|
||||||
|
'Scenario': scen.name,
|
||||||
|
'Source': card.name,
|
||||||
|
'Unique Effect': unique?.description ?? 'None',
|
||||||
|
'Idle Mode Sub Rate': unique?.idle_mode_sub_rate ?? 0,
|
||||||
|
'Races': career.progress_log_info.race_history_array.length,
|
||||||
|
'Race Wins': career.progress_log_info.race_history_array.reduce((w, r) => r.race_history.result_rank === 1 ? w + 1 : w, 0),
|
||||||
|
...indep,
|
||||||
|
...dep,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user