zenno/career/ismdata: start of auto career analysis
This commit is contained in:
Generated
+11
-1
@@ -9,7 +9,8 @@
|
|||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@observablehq/plot": "^0.6.17",
|
"@observablehq/plot": "^0.6.17",
|
||||||
"mathjs": "^15.2.0"
|
"mathjs": "^15.2.0",
|
||||||
|
"zod": "^4.4.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/compat": "^2.1.0",
|
"@eslint/compat": "^2.1.0",
|
||||||
@@ -5303,6 +5304,15 @@
|
|||||||
"integrity": "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==",
|
"integrity": "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"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"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-1
@@ -46,7 +46,8 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@observablehq/plot": "^0.6.17",
|
"@observablehq/plot": "^0.6.17",
|
||||||
"mathjs": "^15.2.0"
|
"mathjs": "^15.2.0",
|
||||||
|
"zod": "^4.4.3"
|
||||||
},
|
},
|
||||||
"allowScripts": {
|
"allowScripts": {
|
||||||
"esbuild@0.28.1": true
|
"esbuild@0.28.1": true
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import * as support from '$lib/data/support';
|
||||||
|
import type { SvelteHTMLElements } from 'svelte/elements';
|
||||||
|
|
||||||
|
const sizes = [70, 35, 16];
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
name: keyof typeof support.SPECIALTY_ICON;
|
||||||
|
size: (typeof sizes)[number];
|
||||||
|
}
|
||||||
|
|
||||||
|
let { name, size, ...rest }: Props & SvelteHTMLElements['picture'] = $props();
|
||||||
|
|
||||||
|
const icon = $derived(support.SPECIALTY_ICON[name]);
|
||||||
|
const avifs = $derived(
|
||||||
|
sizes
|
||||||
|
.filter((sz) => sz >= size)
|
||||||
|
.map((sz) => `/img/support/type/${icon}/${sz}x${sz}.avif ${sz / size}x`)
|
||||||
|
.join(', '),
|
||||||
|
);
|
||||||
|
const pngs = $derived(
|
||||||
|
sizes
|
||||||
|
.filter((sz) => sz >= size)
|
||||||
|
.map((sz) => `/img/support/type/${icon}/${sz}x${sz}.png ${sz / size}x`)
|
||||||
|
.join(', '),
|
||||||
|
);
|
||||||
|
const src = $derived(`/img/support/${icon}/${sizes[0]}x${sizes[0]}.png`);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<picture {...rest}>
|
||||||
|
<source srcset={avifs} type="image/avif" />
|
||||||
|
<source srcset={pngs} type="image/png" />
|
||||||
|
<img {src} alt={name} class="inline" width={size} height={size} loading="lazy" />
|
||||||
|
</picture>
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { SvelteHTMLElements } from 'svelte/elements';
|
||||||
|
|
||||||
|
const sizes = [128, 64, 32] as const;
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
icon_id: number | null | undefined;
|
||||||
|
size: (typeof sizes)[number];
|
||||||
|
alt?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
let { icon_id, size, alt, ...rest }: Props & SvelteHTMLElements['picture'] = $props();
|
||||||
|
|
||||||
|
const icon = $derived(icon_id ?? 0);
|
||||||
|
const avifs = $derived(
|
||||||
|
sizes
|
||||||
|
.filter((sz) => sz >= size)
|
||||||
|
.map((sz) => `/img/support/${icon}/${sz}x${sz}.avif ${sz / size}x`)
|
||||||
|
.join(', '),
|
||||||
|
);
|
||||||
|
const pngs = $derived(
|
||||||
|
sizes
|
||||||
|
.filter((sz) => sz >= size)
|
||||||
|
.map((sz) => `/img/support/${icon}/${sz}x${sz}.png ${sz / size}x`)
|
||||||
|
.join(', '),
|
||||||
|
);
|
||||||
|
const src = $derived(icon !== 0 ? `/img/support/${icon}/${sizes[0]}x${sizes[0]}.png` : `/img/support/10011`);
|
||||||
|
const title = $derived(alt ?? `Support card icon ID ${icon_id}`);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if icon !== 0}
|
||||||
|
<picture {...rest}>
|
||||||
|
<source srcset={avifs} type="image/avif" />
|
||||||
|
<source srcset={pngs} type="image/png" />
|
||||||
|
<img {src} {title} alt={title} class="inline" width={size} height={size} loading="lazy" />
|
||||||
|
</picture>
|
||||||
|
{:else}
|
||||||
|
<div class={`w-[${size}px] h-[${size}px]`}></div>
|
||||||
|
{/if}
|
||||||
@@ -45,6 +45,13 @@ export const PAGES = {
|
|||||||
description: 'Calculate the importance of acceleration effects at the starting gate.',
|
description: 'Calculate the importance of acceleration effects at the starting gate.',
|
||||||
},
|
},
|
||||||
] satisfies Page[],
|
] satisfies Page[],
|
||||||
|
career: [
|
||||||
|
{
|
||||||
|
route: resolve('/career/ismdata'),
|
||||||
|
name: 'Idle Single Mode Analysis',
|
||||||
|
description: 'Analyze Umadump records of Independent Training',
|
||||||
|
},
|
||||||
|
] satisfies Page[],
|
||||||
chara: [
|
chara: [
|
||||||
{
|
{
|
||||||
route: resolve('/chara/affinity'),
|
route: resolve('/chara/affinity'),
|
||||||
|
|||||||
@@ -47,7 +47,6 @@ export interface SupportCard {
|
|||||||
Effect[],
|
Effect[],
|
||||||
Effect[],
|
Effect[],
|
||||||
Effect[],
|
Effect[],
|
||||||
Effect[],
|
|
||||||
];
|
];
|
||||||
/**
|
/**
|
||||||
* Unique effect, for cards that have them.
|
* Unique effect, for cards that have them.
|
||||||
@@ -210,3 +209,18 @@ export interface Hint {
|
|||||||
wit?: number;
|
wit?: number;
|
||||||
sp?: number;
|
sp?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function supports(): Promise<SupportCard[]> {
|
||||||
|
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;
|
||||||
|
|||||||
@@ -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<typeof Skill>;
|
||||||
|
|
||||||
|
const SkillTip = z.strictObject({
|
||||||
|
group_id: z.int32(),
|
||||||
|
rarity: z.int32(),
|
||||||
|
level: z.int32(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type SkillTip = z.infer<typeof SkillTip>;
|
||||||
|
|
||||||
|
/** 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<typeof SupportCard>;
|
||||||
|
|
||||||
|
const GroupOuting = z.strictObject({
|
||||||
|
chara_id: z.int32(),
|
||||||
|
is_outing: z.int32(),
|
||||||
|
story_step: z.int32(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type GroupOuting = z.infer<typeof GroupOuting>;
|
||||||
|
|
||||||
|
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<typeof Evaluation>;
|
||||||
|
|
||||||
|
const TrainingLevel = z.strictObject({
|
||||||
|
command_id: z.int32(),
|
||||||
|
level: z.int32(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TrainingLevel = z.infer<typeof TrainingLevel>;
|
||||||
|
|
||||||
|
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<typeof GuestOuting>;
|
||||||
|
|
||||||
|
/** SingleModeSkillUpgrade */
|
||||||
|
const SkillUpgrade = z.strictObject({
|
||||||
|
condition_id: z.int32(),
|
||||||
|
total_count: z.int32(),
|
||||||
|
current_count: z.int32(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type SkillUpgrade = z.infer<typeof SkillUpgrade>;
|
||||||
|
|
||||||
|
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<typeof SingleModeChara>;
|
||||||
|
|
||||||
|
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<typeof ProgressInfo>;
|
||||||
|
|
||||||
|
const CharaEffectLog = z.strictObject({
|
||||||
|
chara_effect_id: z.int32(),
|
||||||
|
is_active: z.boolean(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type CharaEffectLog = z.infer<typeof CharaEffectLog>;
|
||||||
|
|
||||||
|
/** IdleSingleModeSignedInt */
|
||||||
|
const SignedInt = z.strictObject({
|
||||||
|
sign: z.int32(),
|
||||||
|
value: z.int32(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type SignedInt = z.infer<typeof SignedInt>;
|
||||||
|
|
||||||
|
/** 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<typeof GainInfo>;
|
||||||
|
|
||||||
|
/** IdleSingleModeSupportCardGain */
|
||||||
|
const SupportCardGain = z.strictObject({
|
||||||
|
support_card_id: z.int32(),
|
||||||
|
gain_info: GainInfo,
|
||||||
|
});
|
||||||
|
|
||||||
|
export type SupportCardGain = z.infer<typeof SupportCardGain>;
|
||||||
|
|
||||||
|
const Factor = z.strictObject({
|
||||||
|
factor_id: z.int32(),
|
||||||
|
level: z.int32(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type Factor = z.infer<typeof Factor>;
|
||||||
|
|
||||||
|
/** IdleSingleModeSuccessionFactorGain */
|
||||||
|
const SuccessionFactorGain = z.strictObject({
|
||||||
|
year: z.int32(),
|
||||||
|
gain_factor_info_array: z.array(Factor),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type SuccessionFactorGain = z.infer<typeof SuccessionFactorGain>;
|
||||||
|
|
||||||
|
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<typeof SingleRaceHistory>;
|
||||||
|
|
||||||
|
const RaceReward = z.strictObject({
|
||||||
|
item_type: z.int32(),
|
||||||
|
item_id: z.int32(),
|
||||||
|
item_num: z.int32(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type RaceReward = z.infer<typeof RaceReward>;
|
||||||
|
|
||||||
|
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<typeof RaceRewardLimit>;
|
||||||
|
|
||||||
|
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<typeof CharaRaceReward>;
|
||||||
|
|
||||||
|
/** IdleSingleModeRaceHistory */
|
||||||
|
const RaceHistory = z.strictObject({
|
||||||
|
race_history: SingleRaceHistory,
|
||||||
|
race_reward_info: CharaRaceReward,
|
||||||
|
lose_tips_id: z.int32(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type RaceHistory = z.infer<typeof RaceHistory>;
|
||||||
|
|
||||||
|
/** 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<typeof ProgressLog>;
|
||||||
|
|
||||||
|
const AddItem = z.strictObject({
|
||||||
|
item_id: z.int32(),
|
||||||
|
number: z.int32(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type AddItem = z.infer<typeof AddItem>;
|
||||||
|
|
||||||
|
const RewardSummary = z.strictObject({
|
||||||
|
add_item_list: z.array(AddItem),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type RewardSummary = z.infer<typeof RewardSummary>;
|
||||||
|
|
||||||
|
const End = z.strictObject({
|
||||||
|
chara_info: SingleModeChara,
|
||||||
|
reward_summary_info: RewardSummary,
|
||||||
|
});
|
||||||
|
|
||||||
|
export type End = z.infer<typeof End>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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<typeof Career>;
|
||||||
+70
-33
@@ -20,6 +20,34 @@ export enum Stat {
|
|||||||
*/
|
*/
|
||||||
export const StatList = [Stat.Speed, Stat.Stamina, Stat.Power, Stat.Guts, Stat.Wit] as const;
|
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.
|
* Mood levels.
|
||||||
*/
|
*/
|
||||||
@@ -159,7 +187,16 @@ const powerStatGroundMod = {
|
|||||||
},
|
},
|
||||||
} as const;
|
} 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<Record<AptitudeLevel, number>> = {
|
||||||
|
[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.
|
* Compute adjusted speed for a race.
|
||||||
@@ -282,34 +319,6 @@ export const RUNNING_STYLES = [
|
|||||||
['Runaway', RunningStyle.Runaway],
|
['Runaway', RunningStyle.Runaway],
|
||||||
] as const;
|
] 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.
|
* Race phases.
|
||||||
* While last spurt phase is also a phase, it is not distinguished here.
|
* 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],
|
[1.063, 0.962, 0.95],
|
||||||
] as const;
|
] 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<Record<AptitudeLevel, number>> = {
|
||||||
|
[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.
|
* Get a horse's base target speed outside of late race.
|
||||||
@@ -457,7 +475,7 @@ export function baseTargetSpeed(
|
|||||||
style: RunningStyle,
|
style: RunningStyle,
|
||||||
phase: Phase,
|
phase: Phase,
|
||||||
speedStat?: number,
|
speedStat?: number,
|
||||||
distanceApt?: number,
|
distanceApt?: AptitudeLevel,
|
||||||
): number {
|
): number {
|
||||||
const base = baseSpeed(raceLen);
|
const base = baseSpeed(raceLen);
|
||||||
const baseTarget = base * speedStrategyPhaseCoeff[style][phase];
|
const baseTarget = base * speedStrategyPhaseCoeff[style][phase];
|
||||||
@@ -598,8 +616,27 @@ const accelStrategyPhaseCoeff = {
|
|||||||
[RunningStyle.Runaway]: [1.17, 0.94, 0.956],
|
[RunningStyle.Runaway]: [1.17, 0.94, 0.956],
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
const surfaceProficiencyMod = [0.1, 0.3, 0.5, 0.7, 0.8, 0.9, 1.0, 1.05] as const;
|
const surfaceProficiencyMod: Readonly<Record<AptitudeLevel, number>> = {
|
||||||
const accelDistanceProficiencyMod = [0.4, 0.5, 0.6, 1, 1, 1, 1, 1] as const;
|
[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<Record<AptitudeLevel, number>> = {
|
||||||
|
[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.
|
* Calculate a horse's instantaneous acceleration value.
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
<p>She's read all about Umamusume, and she's always happy to share her knowledge and give recommendations!</p>
|
<p>She's read all about Umamusume, and she's always happy to share her knowledge and give recommendations!</p>
|
||||||
|
|
||||||
<IndexSection title="Race Mechanics Tools" pages={all.PAGES.race} />
|
<IndexSection title="Race Mechanics Tools" pages={all.PAGES.race} />
|
||||||
|
<IndexSection title="Career Tools" pages={all.PAGES.career} />
|
||||||
<IndexSection title="Character Tools" pages={all.PAGES.chara} />
|
<IndexSection title="Character Tools" pages={all.PAGES.chara} />
|
||||||
<IndexSection title="Documents" pages={all.PAGES.doc} />
|
<IndexSection title="Documents" pages={all.PAGES.doc} />
|
||||||
|
|
||||||
|
|||||||
@@ -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 — {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">⇈{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>
|
||||||
Reference in New Issue
Block a user