diff --git a/zenno/package-lock.json b/zenno/package-lock.json
index b775a15..dd95f7f 100644
--- a/zenno/package-lock.json
+++ b/zenno/package-lock.json
@@ -9,7 +9,8 @@
"version": "0.0.1",
"dependencies": {
"@observablehq/plot": "^0.6.17",
- "mathjs": "^15.2.0"
+ "mathjs": "^15.2.0",
+ "zod": "^4.4.3"
},
"devDependencies": {
"@eslint/compat": "^2.1.0",
@@ -5303,6 +5304,15 @@
"integrity": "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==",
"dev": true,
"license": "MIT"
+ },
+ "node_modules/zod": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz",
+ "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/colinhacks"
+ }
}
}
}
diff --git a/zenno/package.json b/zenno/package.json
index c5cb196..c69b63d 100644
--- a/zenno/package.json
+++ b/zenno/package.json
@@ -46,7 +46,8 @@
},
"dependencies": {
"@observablehq/plot": "^0.6.17",
- "mathjs": "^15.2.0"
+ "mathjs": "^15.2.0",
+ "zod": "^4.4.3"
},
"allowScripts": {
"esbuild@0.28.1": true
diff --git a/zenno/src/lib/SpecialtyIcon.svelte b/zenno/src/lib/SpecialtyIcon.svelte
new file mode 100644
index 0000000..5bfb827
--- /dev/null
+++ b/zenno/src/lib/SpecialtyIcon.svelte
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
diff --git a/zenno/src/lib/SupportIcon.svelte b/zenno/src/lib/SupportIcon.svelte
new file mode 100644
index 0000000..1920e26
--- /dev/null
+++ b/zenno/src/lib/SupportIcon.svelte
@@ -0,0 +1,39 @@
+
+
+{#if icon !== 0}
+
+
+
+
+
+{:else}
+
+{/if}
diff --git a/zenno/src/lib/all.ts b/zenno/src/lib/all.ts
index a4aedf5..772088a 100644
--- a/zenno/src/lib/all.ts
+++ b/zenno/src/lib/all.ts
@@ -45,6 +45,13 @@ export const PAGES = {
description: 'Calculate the importance of acceleration effects at the starting gate.',
},
] satisfies Page[],
+ career: [
+ {
+ route: resolve('/career/ismdata'),
+ name: 'Idle Single Mode Analysis',
+ description: 'Analyze Umadump records of Independent Training',
+ },
+ ] satisfies Page[],
chara: [
{
route: resolve('/chara/affinity'),
diff --git a/zenno/src/lib/data/support.ts b/zenno/src/lib/data/support.ts
index 7b626e3..b709b68 100644
--- a/zenno/src/lib/data/support.ts
+++ b/zenno/src/lib/data/support.ts
@@ -47,7 +47,6 @@ export interface SupportCard {
Effect[],
Effect[],
Effect[],
- Effect[],
];
/**
* Unique effect, for cards that have them.
@@ -210,3 +209,18 @@ export interface Hint {
wit?: number;
sp?: number;
}
+
+export async function supports(): Promise {
+ const resp = await fetch('/api/global/support');
+ return resp.json();
+}
+
+export const SPECIALTY_ICON = {
+ Speed: 0,
+ Stamina: 1,
+ Power: 2,
+ Guts: 3,
+ Wit: 4,
+ Pal: 5,
+ Group: 6,
+} as const;
diff --git a/zenno/src/lib/ism.ts b/zenno/src/lib/ism.ts
new file mode 100644
index 0000000..472f51f
--- /dev/null
+++ b/zenno/src/lib/ism.ts
@@ -0,0 +1,312 @@
+import * as z from 'zod';
+import * as race from './race';
+
+const Skill = z.strictObject({
+ skill_id: z.int32(),
+ level: z.int32(),
+});
+
+export type Skill = z.infer;
+
+const SkillTip = z.strictObject({
+ group_id: z.int32(),
+ rarity: z.int32(),
+ level: z.int32(),
+});
+
+export type SkillTip = z.infer;
+
+/** SingleModeSupportCard */
+const SupportCard = z.strictObject({
+ position: z.int32(),
+ support_card_id: z.int32(),
+ limit_break_count: z.int32(),
+ exp: z.int32(),
+ owner_viewer_id: z.number(),
+});
+
+export type SupportCard = z.infer;
+
+const GroupOuting = z.strictObject({
+ chara_id: z.int32(),
+ is_outing: z.int32(),
+ story_step: z.int32(),
+});
+
+export type GroupOuting = z.infer;
+
+const Evaluation = z.strictObject({
+ target_id: z.int32(),
+ training_partner_id: z.int32(),
+ evaluation: z.int32(),
+ is_outing: z.int32(),
+ story_step: z.int32(),
+ is_appear: z.int32(),
+ group_outing_info_array: z.array(GroupOuting),
+});
+
+export type Evaluation = z.infer;
+
+const TrainingLevel = z.strictObject({
+ command_id: z.int32(),
+ level: z.int32(),
+});
+
+export type TrainingLevel = z.infer;
+
+const GuestOuting = z.strictObject({
+ support_card_id: z.int32(),
+ story_step: z.int32(),
+ group_outing_info_array: z.array(GroupOuting),
+});
+
+export type GuestOuting = z.infer;
+
+/** SingleModeSkillUpgrade */
+const SkillUpgrade = z.strictObject({
+ condition_id: z.int32(),
+ total_count: z.int32(),
+ current_count: z.int32(),
+});
+
+export type SkillUpgrade = z.infer;
+
+const AptitudeLevel = z.enum(race.AptitudeLevel);
+
+const SingleModeChara = z.strictObject({
+ single_mode_chara_id: z.int32(),
+ card_id: z.int32(),
+ chara_grade: z.int32(),
+ speed: z.int32(),
+ stamina: z.int32(),
+ power: z.int32(),
+ wiz: z.int32(),
+ guts: z.int32(),
+ vital: z.int32(),
+ max_speed: z.int32(),
+ max_stamina: z.int32(),
+ max_power: z.int32(),
+ max_wiz: z.int32(),
+ max_guts: z.int32(),
+ default_max_speed: z.int32(),
+ default_max_stamina: z.int32(),
+ default_max_power: z.int32(),
+ default_max_wiz: z.int32(),
+ default_max_guts: z.int32(),
+ max_vital: z.int32(),
+ motivation: z.int32(),
+ fans: z.int32(),
+ rarity: z.int32(),
+ race_program_id: z.int32(),
+ reserve_race_program_id: z.int32(),
+ race_running_style: z.int32(),
+ is_short_race: z.int32(),
+ talent_level: z.int32(),
+ skill_array: z.array(Skill),
+ disable_skill_id_array: z.array(z.int32()),
+ skill_tips_array: z.array(SkillTip),
+ support_card_array: z.array(SupportCard),
+ succession_trained_chara_id_1: z.int32(),
+ succession_trained_chara_id_2: z.int32(),
+ proper_distance_short: AptitudeLevel,
+ proper_distance_mile: AptitudeLevel,
+ proper_distance_middle: AptitudeLevel,
+ proper_distance_long: AptitudeLevel,
+ proper_running_style_nige: AptitudeLevel,
+ proper_running_style_senko: AptitudeLevel,
+ proper_running_style_sashi: AptitudeLevel,
+ proper_running_style_oikomi: AptitudeLevel,
+ proper_ground_turf: AptitudeLevel,
+ proper_ground_dirt: AptitudeLevel,
+ turn: z.int32(),
+ skill_point: z.int32(),
+ short_cut_state: z.int32(),
+ state: z.int32(),
+ playing_state: z.int32(),
+ scenario_id: z.int32(),
+ route_id: z.int32(),
+ start_time: z.string(),
+ evaluation_info_array: z.array(Evaluation),
+ training_level_info_array: z.array(TrainingLevel),
+ nickname_id_array: z.array(z.int32()),
+ chara_effect_id_array: z.array(z.int32()),
+ route_race_id_array: z.array(z.int32()),
+ guest_outing_info_array: z.array(GuestOuting),
+ skill_upgrade_info_array: z.array(SkillUpgrade),
+});
+
+export type SingleModeChara = z.infer;
+
+const ProgressInfo = z.strictObject({
+ chara_info: z.optional(SingleModeChara),
+ start_time: z.string(),
+ end_time: z.string(),
+ dress_id: z.int32(),
+});
+
+export type ProgressInfo = z.infer;
+
+const CharaEffectLog = z.strictObject({
+ chara_effect_id: z.int32(),
+ is_active: z.boolean(),
+});
+
+export type CharaEffectLog = z.infer;
+
+/** IdleSingleModeSignedInt */
+const SignedInt = z.strictObject({
+ sign: z.int32(),
+ value: z.int32(),
+});
+
+export type SignedInt = z.infer;
+
+/** IdleSingleModeGainInfo */
+const GainInfo = z.strictObject({
+ speed: SignedInt,
+ stamina: SignedInt,
+ power: SignedInt,
+ wiz: SignedInt,
+ guts: SignedInt,
+ max_speed: z.int32(),
+ max_stamina: z.int32(),
+ max_power: z.int32(),
+ max_wiz: z.int32(),
+ max_guts: z.int32(),
+ proper_distance_short: z.int32(),
+ proper_distance_mile: z.int32(),
+ proper_distance_middle: z.int32(),
+ proper_distance_long: z.int32(),
+ proper_running_style_nige: z.int32(),
+ proper_running_style_senko: z.int32(),
+ proper_running_style_sashi: z.int32(),
+ proper_running_style_oikomi: z.int32(),
+ proper_ground_turf: z.int32(),
+ proper_ground_dirt: z.int32(),
+ skill_point: z.int32(),
+ skill_tips_array: z.array(SkillTip),
+});
+
+export type GainInfo = z.infer;
+
+/** IdleSingleModeSupportCardGain */
+const SupportCardGain = z.strictObject({
+ support_card_id: z.int32(),
+ gain_info: GainInfo,
+});
+
+export type SupportCardGain = z.infer;
+
+const Factor = z.strictObject({
+ factor_id: z.int32(),
+ level: z.int32(),
+});
+
+export type Factor = z.infer;
+
+/** IdleSingleModeSuccessionFactorGain */
+const SuccessionFactorGain = z.strictObject({
+ year: z.int32(),
+ gain_factor_info_array: z.array(Factor),
+});
+
+export type SuccessionFactorGain = z.infer;
+
+const SingleRaceHistory = z.strictObject({
+ turn: z.int32(),
+ program_id: z.int32(),
+ weather: z.int32(),
+ ground_condition: z.int32(),
+ running_style: z.int32(),
+ result_rank: z.int32(),
+ frame_order: z.int32(),
+ npc_count: z.int32(),
+});
+
+export type SingleRaceHistory = z.infer;
+
+const RaceReward = z.strictObject({
+ item_type: z.int32(),
+ item_id: z.int32(),
+ item_num: z.int32(),
+});
+
+export type RaceReward = z.infer;
+
+const RaceRewardLimit = z.strictObject({
+ reward_id: z.int32(),
+ item_type: z.int32(),
+ item_id: z.int32(),
+ item_num: z.int32(),
+ rest_count: z.int32(),
+});
+
+export type RaceRewardLimit = z.infer;
+
+const CharaRaceReward = z.strictObject({
+ result_rank: z.int32(),
+ result_time: z.int32(),
+ race_reward: z.array(RaceReward),
+ race_reward_bonus: z.array(RaceReward),
+ race_reward_plus_bonus: z.array(RaceReward),
+ race_reward_bonus_win: z.array(RaceReward),
+ race_reward_limit: z.optional(z.array(RaceRewardLimit)),
+ gained_fans: z.int32(),
+ campaign_id_array: z.array(z.int32()),
+});
+
+export type CharaRaceReward = z.infer;
+
+/** IdleSingleModeRaceHistory */
+const RaceHistory = z.strictObject({
+ race_history: SingleRaceHistory,
+ race_reward_info: CharaRaceReward,
+ lose_tips_id: z.int32(),
+});
+
+export type RaceHistory = z.infer;
+
+/** IdleSingleModeProgressLog */
+const ProgressLog = z.strictObject({
+ chara_effect_log_array: z.array(CharaEffectLog),
+ support_card_gain_info_array: z.array(SupportCardGain),
+ event_gain_info: GainInfo,
+ succession_gain_info: GainInfo,
+ succession_factor_gain_array: z.array(SuccessionFactorGain),
+ race_history_array: z.array(RaceHistory),
+ gain_skill_id_array: z.array(z.int32()),
+ total_skill_point: z.int32(),
+});
+
+export type ProgressLog = z.infer;
+
+const AddItem = z.strictObject({
+ item_id: z.int32(),
+ number: z.int32(),
+});
+
+export type AddItem = z.infer;
+
+const RewardSummary = z.strictObject({
+ add_item_list: z.array(AddItem),
+});
+
+export type RewardSummary = z.infer;
+
+const End = z.strictObject({
+ chara_info: SingleModeChara,
+ reward_summary_info: RewardSummary,
+});
+
+export type End = z.infer;
+
+/**
+ * WorkIdleSingleMode, or all the data extracted from an ISM career.
+ */
+export const Career = z.object({
+ progress_info: ProgressInfo,
+ progress_log_info: ProgressLog,
+ end_info: End,
+});
+
+export type Career = z.infer;
diff --git a/zenno/src/lib/race.ts b/zenno/src/lib/race.ts
index f3bc38e..e64b4e3 100644
--- a/zenno/src/lib/race.ts
+++ b/zenno/src/lib/race.ts
@@ -20,6 +20,34 @@ export enum Stat {
*/
export const StatList = [Stat.Speed, Stat.Stamina, Stat.Power, Stat.Guts, Stat.Wit] as const;
+/**
+ * Aptitude or proficiency levels.
+ */
+export enum AptitudeLevel {
+ G = 1,
+ F = 2,
+ E = 3,
+ D = 4,
+ C = 5,
+ B = 6,
+ A = 7,
+ S = 8,
+}
+
+/**
+ * Aptitude levels as a descending list for easy iterating.
+ */
+export const APTITUDE_LEVELS = [
+ AptitudeLevel.S,
+ AptitudeLevel.A,
+ AptitudeLevel.B,
+ AptitudeLevel.C,
+ AptitudeLevel.D,
+ AptitudeLevel.E,
+ AptitudeLevel.F,
+ AptitudeLevel.G,
+] as const;
+
/**
* Mood levels.
*/
@@ -159,7 +187,16 @@ const powerStatGroundMod = {
},
} as const;
-const strategyProficiencyMod = [0.1, 0.2, 0.4, 0.6, 0.75, 0.85, 1.0, 1.1] as const;
+const strategyProficiencyMod: Readonly> = {
+ [AptitudeLevel.G]: 0.1,
+ [AptitudeLevel.F]: 0.2,
+ [AptitudeLevel.E]: 0.4,
+ [AptitudeLevel.D]: 0.6,
+ [AptitudeLevel.C]: 0.75,
+ [AptitudeLevel.B]: 0.85,
+ [AptitudeLevel.A]: 1.0,
+ [AptitudeLevel.S]: 1.1,
+} as const;
/**
* Compute adjusted speed for a race.
@@ -282,34 +319,6 @@ export const RUNNING_STYLES = [
['Runaway', RunningStyle.Runaway],
] as const;
-/**
- * Aptitude or proficiency levels.
- */
-export enum AptitudeLevel {
- G,
- F,
- E,
- D,
- C,
- B,
- A,
- S,
-}
-
-/**
- * Aptitude levels as a descending list for easy iterating.
- */
-export const APTITUDE_LEVELS = [
- AptitudeLevel.S,
- AptitudeLevel.A,
- AptitudeLevel.B,
- AptitudeLevel.C,
- AptitudeLevel.D,
- AptitudeLevel.E,
- AptitudeLevel.F,
- AptitudeLevel.G,
-] as const;
-
/**
* Race phases.
* While last spurt phase is also a phase, it is not distinguished here.
@@ -428,7 +437,16 @@ const speedStrategyPhaseCoeff = [
[1.063, 0.962, 0.95],
] as const;
-const distanceProficiencyMod = [0.1, 0.2, 0.4, 0.6, 0.8, 0.9, 1.0, 1.05] as const;
+const distanceProficiencyMod: Readonly> = {
+ [AptitudeLevel.G]: 0.1,
+ [AptitudeLevel.F]: 0.2,
+ [AptitudeLevel.E]: 0.4,
+ [AptitudeLevel.D]: 0.6,
+ [AptitudeLevel.C]: 0.8,
+ [AptitudeLevel.B]: 0.9,
+ [AptitudeLevel.A]: 1.0,
+ [AptitudeLevel.S]: 1.05,
+} as const;
/**
* Get a horse's base target speed outside of late race.
@@ -457,7 +475,7 @@ export function baseTargetSpeed(
style: RunningStyle,
phase: Phase,
speedStat?: number,
- distanceApt?: number,
+ distanceApt?: AptitudeLevel,
): number {
const base = baseSpeed(raceLen);
const baseTarget = base * speedStrategyPhaseCoeff[style][phase];
@@ -598,8 +616,27 @@ const accelStrategyPhaseCoeff = {
[RunningStyle.Runaway]: [1.17, 0.94, 0.956],
} as const;
-const surfaceProficiencyMod = [0.1, 0.3, 0.5, 0.7, 0.8, 0.9, 1.0, 1.05] as const;
-const accelDistanceProficiencyMod = [0.4, 0.5, 0.6, 1, 1, 1, 1, 1] as const;
+const surfaceProficiencyMod: Readonly> = {
+ [AptitudeLevel.G]: 0.1,
+ [AptitudeLevel.F]: 0.3,
+ [AptitudeLevel.E]: 0.5,
+ [AptitudeLevel.D]: 0.7,
+ [AptitudeLevel.C]: 0.8,
+ [AptitudeLevel.B]: 0.9,
+ [AptitudeLevel.A]: 1.0,
+ [AptitudeLevel.S]: 1.05,
+} as const;
+
+const accelDistanceProficiencyMod: Readonly> = {
+ [AptitudeLevel.G]: 0.4,
+ [AptitudeLevel.F]: 0.5,
+ [AptitudeLevel.E]: 0.6,
+ [AptitudeLevel.D]: 1.0,
+ [AptitudeLevel.C]: 1.0,
+ [AptitudeLevel.B]: 1.0,
+ [AptitudeLevel.A]: 1.0,
+ [AptitudeLevel.S]: 1.0,
+} as const;
/**
* Calculate a horse's instantaneous acceleration value.
diff --git a/zenno/src/routes/+page.svelte b/zenno/src/routes/+page.svelte
index 83fe30a..bcbee3c 100644
--- a/zenno/src/routes/+page.svelte
+++ b/zenno/src/routes/+page.svelte
@@ -7,6 +7,7 @@
She's read all about Umamusume, and she's always happy to share her knowledge and give recommendations!
+
diff --git a/zenno/src/routes/career/+page.svelte b/zenno/src/routes/career/+page.svelte
new file mode 100644
index 0000000..2e856ec
--- /dev/null
+++ b/zenno/src/routes/career/+page.svelte
@@ -0,0 +1,11 @@
+
+
+Zenno Rob Roy — {cur.name}
+{cur.description}
+
diff --git a/zenno/src/routes/career/ismdata/+page.svelte b/zenno/src/routes/career/ismdata/+page.svelte
new file mode 100644
index 0000000..2a173a0
--- /dev/null
+++ b/zenno/src/routes/career/ismdata/+page.svelte
@@ -0,0 +1,164 @@
+
+
+Idle Single Mode Analysis
+
+
+{#each errors as err (err.f.name)}
+
+
{err.f.name}
+
+ {#if err.err instanceof z.ZodError}
+ {#each err.err.issues as issue (issue.path)}
+ - {issue.path}: {issue.message}
+ {/each}
+ {/if}
+
+
+{/each}
+
+{#if loaded.length > 0}
+
+
+
+ {(files?.length ?? 0) - errors.length} careers
+ {#if exclude.size !== 0}
+ ({loaded.length - careers.length} excluded)
+ {/if}
+
+ {#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)}
+ !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)}
+
+
+ {'◆'.repeat(card.limit_break_count)}{'◇'.repeat(4 - card.limit_break_count)}
+
+ {/snippet}
+ {#snippet skillHint(hint: ism.SkillTip)}
+ {@const g = skillGroups.get(hint.group_id)}
+ {@const s = groupskill(g, hint.rarity)}
+
+ {#if s != null}
+
+ {s.name}
+ +{hint.level}
+ {/if}
+
+ {/snippet}
+ {#snippet spark(id: number)}
+ {@const spark = sparks.get(id)}
+ {#if spark != null}
+
+ {/if}
+ {/snippet}
+
+ {/each}
+
+
+{/if}
diff --git a/zenno/src/routes/career/ismdata/CareerOverview.svelte b/zenno/src/routes/career/ismdata/CareerOverview.svelte
new file mode 100644
index 0000000..6904e46
--- /dev/null
+++ b/zenno/src/routes/career/ismdata/CareerOverview.svelte
@@ -0,0 +1,110 @@
+
+
+{#snippet gainStat(name: ComponentProps['name'], stat: number, max: number)}
+
+
+ +{stat}
+ {#if max !== 0}
+ ⇈{max}
+ {/if}
+
+{/snippet}
+{#snippet gainInfo(gain: ism.GainInfo)}
+
+
+ {@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)}
+
+ SP
+ +{gain.skill_point}
+
+
+
+ {#each gain.skill_tips_array as hint (hint.group_id * 10 + hint.rarity)}
+ {@render skillHint(hint)}
+ {/each}
+
+
+{/snippet}
+
+
+
+ {career.progress_info.start_time} #{career.end_info.chara_info.single_mode_chara_id}
+
+ {@render chara()}
+
+
+ {@render scenario()}
+
+
+
+
+ {#each career.end_info.chara_info.support_card_array as card (card.support_card_id)}
+ {@render support(card)}
+ {/each}
+
+
+ Gains per source
+ {#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)!}
+
+
+ {@render support(cardInfo)}
+
+
+ {@render gainInfo(card.gain_info)}
+
+
+ {/each}
+
+
Events
+
+ {@render gainInfo(career.progress_log_info.event_gain_info)}
+
+
+
+
Inspiration
+
+ {@render gainInfo(career.progress_log_info.succession_gain_info)}
+
+
+
+
+ Hints for {career.end_info.chara_info.skill_tips_array.length} skills
+
+ {#each career.end_info.chara_info.skill_tips_array as hint (hint.group_id * 10 + hint.rarity)}
+ {@render skillHint(hint)}
+ {/each}
+
+
+
+ {totalSparks} sparks activated
+ {#each career.progress_log_info.succession_factor_gain_array as inspo (inspo.year)}
+
+ {/each}
+
+
diff --git a/zenno/src/routes/career/ismdata/CareerOverviewInspoYear.svelte b/zenno/src/routes/career/ismdata/CareerOverviewInspoYear.svelte
new file mode 100644
index 0000000..0320626
--- /dev/null
+++ b/zenno/src/routes/career/ismdata/CareerOverviewInspoYear.svelte
@@ -0,0 +1,18 @@
+
+
+
+
Year {inspo.year}
+ {#each inspo.gain_factor_info_array as s, i (i)}
+ {@render spark(s.factor_id)}
+ {/each}
+