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 @@ + + + + + + {name} + 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} + + + + {title} + +{: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} +