zenno/career/ismdata: start visualization
This commit is contained in:
@@ -13,6 +13,8 @@
|
||||
import SupportIcon from '$lib/SupportIcon.svelte';
|
||||
import SkillIcon from '$lib/SkillIcon.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 skills = new SvelteMap<number, skill.Skill>();
|
||||
@@ -55,6 +57,7 @@
|
||||
});
|
||||
|
||||
let files: FileList | undefined = $state();
|
||||
let careersOpen = $state(false);
|
||||
const parses = $derived.by(async () => {
|
||||
if (files == null) {
|
||||
return [];
|
||||
@@ -75,7 +78,11 @@
|
||||
let exclude = new SvelteSet<string>();
|
||||
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 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']) {
|
||||
if (g == null) {
|
||||
@@ -116,13 +123,14 @@
|
||||
|
||||
{#if loaded.length > 0}
|
||||
<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>
|
||||
{(files?.length ?? 0) - errors.length} careers
|
||||
{#if exclude.size !== 0}
|
||||
({loaded.length - careers.length} excluded)
|
||||
{/if}
|
||||
</summary>
|
||||
{#if careersOpen}
|
||||
{#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 scen = scenarios.get(career.ok.end_info.chara_info.scenario_id)}
|
||||
@@ -159,6 +167,24 @@
|
||||
{/snippet}
|
||||
</CareerOverview>
|
||||
{/each}
|
||||
{/if}
|
||||
</details>
|
||||
</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}
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
const totalSparks = $derived(
|
||||
career.progress_log_info.succession_factor_gain_array.reduce((a, y) => a + y.gain_factor_info_array.length, 0),
|
||||
);
|
||||
|
||||
let open = $state(false);
|
||||
</script>
|
||||
|
||||
{#snippet gainStat(name: ComponentProps<typeof SpecialtyIcon>['name'], stat: number, max: number)}
|
||||
@@ -67,8 +69,9 @@
|
||||
{@render support(card)}
|
||||
{/each}
|
||||
</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>
|
||||
{#if open}
|
||||
{#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)!}
|
||||
<div class="flex w-full flex-row pr-4">
|
||||
@@ -92,6 +95,7 @@
|
||||
{@render gainInfo(career.progress_log_info.succession_gain_info)}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</details>
|
||||
<details class="mx-2 mb-1 w-full pr-4">
|
||||
<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