zenno/career/ismdata: start of auto career analysis
ci/woodpecker/push/horsebot Pipeline was successful
ci/woodpecker/push/zenno Pipeline failed

This commit is contained in:
2026-08-07 17:01:33 -04:00
parent 5a13519189
commit 1fe397ca81
13 changed files with 794 additions and 36 deletions
+11
View File
@@ -0,0 +1,11 @@
<script lang="ts">
import { page } from '$app/state';
import * as all from '$lib/all';
import IndexSection from '$lib/IndexSection.svelte';
const cur = $derived(all.page(page.route.id));
</script>
<h1 class="m-8 text-center text-7xl">Zenno Rob Roy &mdash; {cur.name}</h1>
<p>{cur.description}</p>
<IndexSection pages={all.PAGES.doc} />
@@ -0,0 +1,164 @@
<script lang="ts">
import * as z from 'zod';
import * as ism from '$lib/ism';
import * as character from '$lib/data/character';
import * as skill from '$lib/data/skill';
import * as support from '$lib/data/support';
import * as scenario from '$lib/data/scenario';
import * as spark from '$lib/data/spark';
import * as uma from '$lib/data/uma';
import { onMount } from 'svelte';
import { SvelteMap, SvelteSet } from 'svelte/reactivity';
import CareerOverview from './CareerOverview.svelte';
import SupportIcon from '$lib/SupportIcon.svelte';
import SkillIcon from '$lib/SkillIcon.svelte';
import Spark from '$lib/Spark.svelte';
let characters = new SvelteMap<number, character.Character>();
let skills = new SvelteMap<number, skill.Skill>();
let skillGroups = new SvelteMap<number, skill.SkillGroup>();
let supports = new SvelteMap<number, support.SupportCard>();
let scenarios = new SvelteMap<number, scenario.Scenario>();
let sparks = new SvelteMap<number, spark.Spark>();
let umas = new SvelteMap<number, uma.Uma>();
onMount(async () => {
const [c, sk, sg, sc, ss, sp, u] = await Promise.all([
character.character(),
skill.skills(),
skill.skillGroups(),
support.supports(),
scenario.scenarios(),
spark.sparks(),
uma.uma(),
]);
for (const chara of c) {
characters.set(chara.chara_id, chara);
}
for (const skill of sk) {
skills.set(skill.skill_id, skill);
}
for (const group of sg) {
skillGroups.set(group.skill_group, group);
}
for (const card of sc) {
supports.set(card.id, card);
}
for (const scen of ss) {
scenarios.set(scen.scenario_id, scen);
}
for (const spark of sp) {
sparks.set(spark.spark_id, spark);
}
for (const uma of u) {
umas.set(uma.chara_card_id, uma);
}
});
let files: FileList | undefined = $state();
const parses = $derived.by(async () => {
if (files == null) {
return [];
}
const r = [];
for (const f of files) {
try {
const b = await f.text();
const v = JSON.parse(b);
r.push({ f, ok: ism.Career.parse(v) });
} catch (err) {
console.warn({ f, err });
r.push({ f, err });
}
}
return r;
});
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)));
function groupskill(g: skill.SkillGroup | undefined, rarity: ism.SkillTip['rarity']) {
if (g == null) {
return undefined;
}
const s = [g.skill1, g.skill2, g.skill3, g.skill_bad].map((id) => skills.get(id ?? 0)).filter((x) => x != null);
return s.find((skill) => skill.rarity === rarity);
}
function setExclude(name: string, checked: boolean) {
if (checked) {
exclude.delete(name);
} else {
exclude.add(name);
}
}
</script>
<h1 class="text-4xl">Idle Single Mode Analysis</h1>
<form class="mx-auto mt-8 rounded-md p-4 text-center shadow-md ring md:max-w-2xl">
<input type="file" multiple bind:files accept="application/json" class="h-8 w-full hover:cursor-pointer" />
</form>
{#each errors as err (err.f.name)}
<div
class="mx-auto my-2 w-3xl border-2 border-amber-300 bg-amber-200 p-4 text-amber-800 dark:border-amber-800 dark:bg-amber-900 dark:text-amber-200"
>
<span class="text-lg">{err.f.name}</span>
<ul class="list-disc pl-4">
{#if err.err instanceof z.ZodError}
{#each err.err.issues as issue (issue.path)}
<li>{issue.path}: {issue.message}</li>
{/each}
{/if}
</ul>
</div>
{/each}
{#if loaded.length > 0}
<div class="mx-auto my-8 max-w-6xl rounded-md border p-4">
<details class="w-full">
<summary>
{(files?.length ?? 0) - errors.length} careers
{#if exclude.size !== 0}
({loaded.length - careers.length} excluded)
{/if}
</summary>
{#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)}
<CareerOverview career={career.ok} bind:enable={() => !exclude.has(career.f.name), (v) => setExclude(career.f.name, v)}>
{#snippet chara()}
{uma?.name}
{/snippet}
{#snippet scenario()}
{scen?.name}
{/snippet}
{#snippet support(card: ism.SupportCard)}
{@const s = supports.get(card.support_card_id)}
<div class="flex flex-col text-center">
<SupportIcon icon_id={card.support_card_id} size={64} alt={s?.name} />
<span class="block text-sm">{'◆'.repeat(card.limit_break_count)}{'◇'.repeat(4 - card.limit_break_count)}</span>
</div>
{/snippet}
{#snippet skillHint(hint: ism.SkillTip)}
{@const g = skillGroups.get(hint.group_id)}
{@const s = groupskill(g, hint.rarity)}
<div class="block rounded-md border p-2">
{#if s != null}
<SkillIcon class="m-2" icon_id={s.icon_id} size={32} />
<span>{s.name}</span>
<span class="ml-auto">+{hint.level}</span>
{/if}
</div>
{/snippet}
{#snippet spark(id: number)}
{@const spark = sparks.get(id)}
{#if spark != null}
<Spark {spark} />
{/if}
{/snippet}
</CareerOverview>
{/each}
</details>
</div>
{/if}
@@ -0,0 +1,110 @@
<script lang="ts">
import * as ism from '$lib/ism';
import SpecialtyIcon from '$lib/SpecialtyIcon.svelte';
import type { ComponentProps, Snippet } from 'svelte';
import CareerOverviewInspoYear from './CareerOverviewInspoYear.svelte';
interface Props {
career: ism.Career;
chara: Snippet;
scenario: Snippet;
support: Snippet<[ism.SupportCard]>;
skillHint: Snippet<[ism.SkillTip]>;
spark: Snippet<[number]>;
enable: boolean;
}
let { career, chara, scenario, support, skillHint, spark, enable = $bindable() }: Props = $props();
const totalSparks = $derived(
career.progress_log_info.succession_factor_gain_array.reduce((a, y) => a + y.gain_factor_info_array.length, 0),
);
</script>
{#snippet gainStat(name: ComponentProps<typeof SpecialtyIcon>['name'], stat: number, max: number)}
<div class="flex gap-2 rounded-md border p-2">
<SpecialtyIcon class="my-auto flex-1" {name} size={35} />
<span class="my-auto flex-1">+{stat}</span>
{#if max !== 0}
<span class="my-auto flex-1">&upuparrows;{max}</span>
{/if}
</div>
{/snippet}
{#snippet gainInfo(gain: ism.GainInfo)}
<div class="grid grid-cols-1 gap-2 p-2">
<div class="grid w-full flex-none grid-cols-6 gap-2">
{@render gainStat('Speed', gain.speed.value, gain.max_speed)}
{@render gainStat('Stamina', gain.stamina.value, gain.max_stamina)}
{@render gainStat('Power', gain.power.value, gain.max_power)}
{@render gainStat('Guts', gain.guts.value, gain.max_guts)}
{@render gainStat('Wit', gain.wiz.value, gain.max_wiz)}
<div class="flex gap-2 rounded-md border p-2">
<span class="my-auto flex-1">SP</span>
<span class="my-auto flex-1">+{gain.skill_point}</span>
</div>
</div>
<div class="grid w-full grid-cols-1 gap-2 md:grid-cols-3">
{#each gain.skill_tips_array as hint (hint.group_id * 10 + hint.rarity)}
{@render skillHint(hint)}
{/each}
</div>
</div>
{/snippet}
<div class="grid grid-cols-1 border shadow-sm transition-shadow hover:shadow-lg">
<div class="my-1 grid w-full grid-cols-2 gap-2 px-2 text-center text-xl md:grid-cols-4">
<span class="my-auto">{career.progress_info.start_time} #{career.end_info.chara_info.single_mode_chara_id}</span>
<span class="my-auto">
{@render chara()}
</span>
<span class="my-auto">
{@render scenario()}
</span>
<input type="checkbox" bind:checked={enable} class="mx-auto my-auto h-6 w-6 align-middle" />
</div>
<div class="mx-2 mt-1 grid w-full grid-cols-2 gap-4 md:grid-cols-3 lg:grid-cols-6">
{#each career.end_info.chara_info.support_card_array as card (card.support_card_id)}
{@render support(card)}
{/each}
</div>
<details class="mx-2 mb-1 grid w-full grid-cols-1 gap-x-2">
<summary>Gains per source</summary>
{#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">
<div class="my-auto w-24 flex-none text-center">
{@render support(cardInfo)}
</div>
<div class="grow">
{@render gainInfo(card.gain_info)}
</div>
</div>
{/each}
<div class="flex w-full flex-row pr-4">
<div class="my-auto w-24 flex-none text-center">Events</div>
<div class="grow">
{@render gainInfo(career.progress_log_info.event_gain_info)}
</div>
</div>
<div class="flex w-full flex-row pr-4">
<div class="my-auto w-24 flex-none text-center">Inspiration</div>
<div class="grow">
{@render gainInfo(career.progress_log_info.succession_gain_info)}
</div>
</div>
</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>
<div class="grid w-full grid-cols-1 gap-2 md:grid-cols-3">
{#each career.end_info.chara_info.skill_tips_array as hint (hint.group_id * 10 + hint.rarity)}
{@render skillHint(hint)}
{/each}
</div>
</details>
<details class="mx-2 mb-1 w-full pr-4">
<summary>{totalSparks} sparks activated</summary>
{#each career.progress_log_info.succession_factor_gain_array as inspo (inspo.year)}
<CareerOverviewInspoYear {inspo} {spark} />
{/each}
</details>
</div>
@@ -0,0 +1,18 @@
<script lang="ts">
import * as ism from '$lib/ism';
import type { Snippet } from 'svelte';
interface Props {
inspo: ism.SuccessionFactorGain;
spark: Snippet<[number]>;
}
let { inspo, spark }: Props = $props();
</script>
<div class="grid w-full grid-cols-4 gap-2 pb-2">
<div class="col-span-full mx-4 w-full text-lg">Year {inspo.year}</div>
{#each inspo.gain_factor_info_array as s, i (i)}
{@render spark(s.factor_id)}
{/each}
</div>