7 Commits
Author SHA1 Message Date
zephyr 53c7079f46 umadump/replay: decode race replays
ci/woodpecker/push/horsebot Pipeline was successful
ci/woodpecker/push/zenno Pipeline was successful
2026-10-03 22:34:02 -04:00
zephyr 1cfabb18f5 zenno: fix type error on fake data
ci/woodpecker/push/horsebot Pipeline was successful
ci/woodpecker/push/zenno Pipeline was successful
2026-09-22 22:31:12 -04:00
zephyr bf4e750825 zenno/chara/convo: hyperlink to umadump 2026-09-22 22:30:00 -04:00
zephyr 57ffadb06a zenno/chara/convo: integrate umadump talk gallery
ci/woodpecker/push/horsebot Pipeline was successful
ci/woodpecker/push/zenno Pipeline failed
Fixes #47.
2026-09-22 22:25:43 -04:00
zephyr 86583e68d1 zenno: wrapper component for race length inputs
ci/woodpecker/push/horsebot Pipeline was successful
ci/woodpecker/push/zenno Pipeline failed
2026-09-22 10:59:29 -04:00
zephyr fde5b4fd20 zenno: update types for new api fields 2026-09-21 10:31:14 -04:00
zephyr 1cf3af5f79 zenno: update full charge info with new knowledge
ci/woodpecker/push/horsebot Pipeline was successful
ci/woodpecker/push/zenno Pipeline was successful
2026-09-21 09:48:56 -04:00
23 changed files with 18101 additions and 360 deletions
+13
View File
@@ -0,0 +1,13 @@
package umadump
type Never struct{}
func (Never) AppendText(b []byte) ([]byte, error) {
panic("umadump: attempt to encode value that should never exist")
}
func (Never) MarshalText() ([]byte, error) {
panic("umadump: attempt to encode value that should never exist")
}
func (Never) UnmarshalText(b []byte) error {
panic("umadump: attempt to decode value that should never exist")
}
File diff suppressed because one or more lines are too long
+29
View File
@@ -0,0 +1,29 @@
// Code generated by "stringer -type CardRarity -linecomment -trimprefix Card"; DO NOT EDIT.
package replay
import "strconv"
func _() {
// An "invalid array index" compiler error signifies that the constant values have changed.
// Re-run the stringer command to generate them again.
var x [1]struct{}
_ = x[CardNone-0]
_ = x[CardRare1-1]
_ = x[CardRare2-2]
_ = x[CardRare3-3]
_ = x[CardRare4-4]
_ = x[CardRare5-5]
}
const _CardRarity_name = "NONERare1Rare2Rare3Rare4Rare5"
var _CardRarity_index = [...]uint8{0, 4, 9, 14, 19, 24, 29}
func (i CardRarity) String() string {
idx := int(i) - 0
if i < 0 || idx >= len(_CardRarity_index)-1 {
return "CardRarity(" + strconv.FormatInt(int64(i), 10) + ")"
}
return _CardRarity_name[_CardRarity_index[idx]:_CardRarity_index[idx+1]]
}
+29
View File
@@ -0,0 +1,29 @@
// Code generated by "stringer -type Motivation -linecomment"; DO NOT EDIT.
package replay
import "strconv"
func _() {
// An "invalid array index" compiler error signifies that the constant values have changed.
// Re-run the stringer command to generate them again.
var x [1]struct{}
_ = x[MotivationNone-0]
_ = x[MotivationAwful-1]
_ = x[MotivationBad-2]
_ = x[MotivationNormal-3]
_ = x[MotivationGood-4]
_ = x[MotivationGreat-5]
}
const _Motivation_name = "NoneMinLowMiddleHighMax"
var _Motivation_index = [...]uint8{0, 4, 7, 10, 16, 20, 23}
func (i Motivation) String() string {
idx := int(i) - 0
if i < 0 || idx >= len(_Motivation_index)-1 {
return "Motivation(" + strconv.FormatInt(int64(i), 10) + ")"
}
return _Motivation_name[_Motivation_index[idx]:_Motivation_index[idx+1]]
}
+32
View File
@@ -0,0 +1,32 @@
// Code generated by "stringer -type ProperGrade -trimprefix Aptitude"; DO NOT EDIT.
package replay
import "strconv"
func _() {
// An "invalid array index" compiler error signifies that the constant values have changed.
// Re-run the stringer command to generate them again.
var x [1]struct{}
_ = x[AptitudeNull-0]
_ = x[AptitudeG-1]
_ = x[AptitudeF-2]
_ = x[AptitudeE-3]
_ = x[AptitudeD-4]
_ = x[AptitudeC-5]
_ = x[AptitudeB-6]
_ = x[AptitudeA-7]
_ = x[AptitudeS-8]
}
const _ProperGrade_name = "NullGFEDCBAS"
var _ProperGrade_index = [...]uint8{0, 4, 5, 6, 7, 8, 9, 10, 11, 12}
func (i ProperGrade) String() string {
idx := int(i) - 0
if i < 0 || idx >= len(_ProperGrade_index)-1 {
return "ProperGrade(" + strconv.FormatInt(int64(i), 10) + ")"
}
return _ProperGrade_name[_ProperGrade_index[idx]:_ProperGrade_index[idx+1]]
}
+355 -268
View File
@@ -6,6 +6,7 @@ import (
"sync"
"git.sunturtle.xyz/zephyr/horse"
"git.sunturtle.xyz/zephyr/horse/umadump"
)
type Race struct {
@@ -46,10 +47,10 @@ type Race struct {
PostNumberMax int32 `json:"postNumberMax"`
PlayerHorseIndex int32 `json:"playerHorseIndex"`
OverridePlayerHorseIndex int32 `json:"overridePlayerHorseIndex"`
PlayerTeamMemberArray []RaceHorse `json:"playerTeamMemberArray"`
PlayerTeamTopFinishOrderHorse RaceHorse `json:"playerTeamTopFinishOrderHorse"`
PlayerTeamMemberArray []HorseData `json:"playerTeamMemberArray"`
PlayerTeamTopFinishOrderHorse HorseData `json:"playerTeamTopFinishOrderHorse"`
IsGateInPopularityInitialized bool `json:"isGateInPopularityInitialized"`
RaceHorse []RaceHorse `json:"raceHorse"`
RaceHorse []HorseData `json:"raceHorse"`
RaceBibMaster struct{} `json:"raceBibMaster"`
RaceMaster struct{} `json:"raceMaster"`
RaceInstanceMaster struct{} `json:"raceInstanceMaster"`
@@ -64,7 +65,7 @@ type Race struct {
ReplayCheckInfoLegend struct{} `json:"replayCheckInfoLegend"`
IsDailyLegendRace bool `json:"isDailyLegendRace"`
ReplayCheckInfoChallengeMatch struct{} `json:"replayCheckInfoChallengeMatch"`
RaceRewardSingle RaceRewardSingle `json:"raceRewardSingle"`
RaceRewardSingle struct{} `json:"raceRewardSingle"`
ResultHorseIndex int32 `json:"resultHorseIndex"`
PrevGradeType CharaGradeType `json:"prevGradeType"`
MainStoryRaceGimmickType MainStoryRaceGimmickType `json:"mainStoryRaceGimmickType"`
@@ -537,122 +538,334 @@ func (r *RaceDifficulty) UnmarshalText(b []byte) error {
}
type RaceParam struct {
RawSpeed int `json:"rawSpeed"`
RawStamina int `json:"rawStamina"`
RawPow int `json:"rawPow"`
RawGuts int `json:"rawGuts"`
RawWiz int `json:"rawWiz"`
BaseSpeed float32 `json:"baseSpeed"`
BaseStamina float32 `json:"baseStamina"`
BasePow float32 `json:"basePow"`
BaseGuts float32 `json:"baseGuts"`
BaseWiz float32 `json:"baseWiz"`
Motivation string `json:"motivation"`
MotivationCoef float32 `json:"motivationCoef"`
RawSpeed int `json:"rawSpeed"`
RawStamina int `json:"rawStamina"`
RawPow int `json:"rawPow"`
RawGuts int `json:"rawGuts"`
RawWiz int `json:"rawWiz"`
BaseSpeed float32 `json:"baseSpeed"`
BaseStamina float32 `json:"baseStamina"`
BasePow float32 `json:"basePow"`
BaseGuts float32 `json:"baseGuts"`
BaseWiz float32 `json:"baseWiz"`
Motivation Motivation `json:"motivation"`
MotivationCoef float32 `json:"motivationCoef"`
}
type Motivation int32
//go:generate go run golang.org/x/tools/cmd/stringer@v0.41.0 -type Motivation -linecomment
const (
MotivationNone Motivation = iota // None
MotivationAwful // Min
MotivationBad // Low
MotivationNormal // Middle
MotivationGood // High
MotivationGreat // Max
)
func (r Motivation) AppendText(b []byte) ([]byte, error) {
return append(b, r.String()...), nil
}
func (r Motivation) MarshalText() ([]byte, error) {
return r.AppendText(nil)
}
func (r *Motivation) UnmarshalText(b []byte) error {
switch string(b) {
case "Min":
*r = MotivationAwful
case "Low":
*r = MotivationBad
case "Middle":
*r = MotivationNormal
case "High":
*r = MotivationGood
case "Max":
*r = MotivationGreat
case "None":
*r = MotivationNone
default:
*r = 0
return fmt.Errorf("unknown motivation %q", b)
}
return nil
}
type SkillArray struct {
SkillID int `json:"skill_id"`
Level int `json:"level"`
SkillID horse.SkillID `json:"skill_id"`
Level int32 `json:"level"`
}
type RaceResultArray struct {
Turn int `json:"turn"`
ProgramID int `json:"program_id"`
Weather int `json:"weather"`
GroundCondition int `json:"ground_condition"`
RunningStyle int `json:"running_style"`
Popularity int `json:"popularity"`
ResultRank int `json:"result_rank"`
ResultTime int `json:"result_time"`
PrizeMoney int `json:"prize_money"`
type RaceResult struct {
Turn int32 `json:"turn"`
ProgramID int32 `json:"program_id"`
Weather int32 `json:"weather"`
GroundCondition int32 `json:"ground_condition"`
RunningStyle int32 `json:"running_style"`
Popularity int32 `json:"popularity"`
ResultRank int32 `json:"result_rank"`
ResultTime int32 `json:"result_time"`
PrizeMoney int32 `json:"prize_money"`
}
type ResponseHorseData struct {
FrameOrder int `json:"frame_order"`
ViewerID int64 `json:"viewer_id"`
TrainerName string `json:"trainer_name"`
OwnerViewerID int `json:"owner_viewer_id"`
OwnerTrainerName string `json:"owner_trainer_name"`
SingleModeCharaID int `json:"single_mode_chara_id"`
TrainedCharaID int `json:"trained_chara_id"`
NicknameID int `json:"nickname_id"`
CharaID int `json:"chara_id"`
CardID int `json:"card_id"`
MobID int `json:"mob_id"`
Rarity int `json:"rarity"`
TalentLevel int `json:"talent_level"`
SkillArray []SkillArray `json:"skill_array"`
Stamina int `json:"stamina"`
Speed int `json:"speed"`
Pow int `json:"pow"`
Guts int `json:"guts"`
Wiz int `json:"wiz"`
RunningStyle int `json:"running_style"`
RaceDressID int `json:"race_dress_id"`
CharaColorType int `json:"chara_color_type"`
NpcType int `json:"npc_type"`
FinalGrade int `json:"final_grade"`
Popularity int `json:"popularity"`
PopularityMarkRankArray []int `json:"popularity_mark_rank_array"`
ProperDistanceShort int `json:"proper_distance_short"`
ProperDistanceMile int `json:"proper_distance_mile"`
ProperDistanceMiddle int `json:"proper_distance_middle"`
ProperDistanceLong int `json:"proper_distance_long"`
ProperRunningStyleNige int `json:"proper_running_style_nige"`
ProperRunningStyleSenko int `json:"proper_running_style_senko"`
ProperRunningStyleSashi int `json:"proper_running_style_sashi"`
ProperRunningStyleOikomi int `json:"proper_running_style_oikomi"`
ProperGroundTurf int `json:"proper_ground_turf"`
ProperGroundDirt int `json:"proper_ground_dirt"`
Motivation int `json:"motivation"`
WinSaddleIDArray []int `json:"win_saddle_id_array"`
RaceResultArray []RaceResultArray `json:"race_result_array"`
TeamID int `json:"team_id"`
TeamMemberID int `json:"team_member_id"`
TeamRank int `json:"team_rank"`
SingleModeWinCount int `json:"single_mode_win_count"`
ItemIDArray []any `json:"item_id_array"`
MotivationChangeFlag int `json:"motivation_change_flag"`
FrameOrderChangeFlag int `json:"frame_order_change_flag"`
type RaceHorseData struct {
FrameOrder int32 `json:"frame_order"`
ViewerID int64 `json:"viewer_id"`
TrainerName string `json:"trainer_name"`
OwnerViewerID int64 `json:"owner_viewer_id"`
OwnerTrainerName string `json:"owner_trainer_name"`
SingleModeCharaID int32 `json:"single_mode_chara_id"`
TrainedCharaID int32 `json:"trained_chara_id"`
NicknameID int32 `json:"nickname_id"`
CharaID int32 `json:"chara_id"`
CardID int32 `json:"card_id"`
MobID int32 `json:"mob_id"`
Rarity int32 `json:"rarity"`
TalentLevel int32 `json:"talent_level"`
SkillArray []SkillArray `json:"skill_array"`
Stamina int32 `json:"stamina"`
Speed int32 `json:"speed"`
Pow int32 `json:"pow"`
Guts int32 `json:"guts"`
Wiz int32 `json:"wiz"`
RunningStyle int32 `json:"running_style"`
RaceDressID int32 `json:"race_dress_id"`
CharaColorType int32 `json:"chara_color_type"`
NpcType int32 `json:"npc_type"`
FinalGrade int32 `json:"final_grade"`
Popularity int32 `json:"popularity"`
PopularityMarkRankArray []int32 `json:"popularity_mark_rank_array"`
ProperDistanceShort int32 `json:"proper_distance_short"`
ProperDistanceMile int32 `json:"proper_distance_mile"`
ProperDistanceMiddle int32 `json:"proper_distance_middle"`
ProperDistanceLong int32 `json:"proper_distance_long"`
ProperRunningStyleNige int32 `json:"proper_running_style_nige"`
ProperRunningStyleSenko int32 `json:"proper_running_style_senko"`
ProperRunningStyleSashi int32 `json:"proper_running_style_sashi"`
ProperRunningStyleOikomi int32 `json:"proper_running_style_oikomi"`
ProperGroundTurf int32 `json:"proper_ground_turf"`
ProperGroundDirt int32 `json:"proper_ground_dirt"`
Motivation int32 `json:"motivation"`
WinSaddleIDArray []int32 `json:"win_saddle_id_array"`
RaceResultArray []RaceResult `json:"race_result_array"`
TeamID int32 `json:"team_id"`
TeamMemberID int32 `json:"team_member_id"`
TeamRank int32 `json:"team_rank"`
SingleModeWinCount int32 `json:"single_mode_win_count"`
ItemIDArray []int32 `json:"item_id_array"`
MotivationChangeFlag int32 `json:"motivation_change_flag"`
FrameOrderChangeFlag int32 `json:"frame_order_change_flag"`
}
type RaceRecord struct {
type HorseData struct {
HorseIndex int32 `json:"horseIndex"`
PostNumber int32 `json:"postNumber"`
CharaID horse.CharacterID `json:"charaId"`
CharaName string `json:"charaName"`
FinishOrder int32 `json:"finishOrder"`
FinishTimeRaw float32 `json:"finishTimeRaw"`
FinishTimeScaled float32 `json:"finishTimeScaled"`
FinishDiffTimeFromPrev float32 `json:"finishDiffTimeFromPrev"`
RaceParam RaceParam `json:"raceParam"`
ResponseHorseData RaceHorseData `json:"responseHorseData"`
Popularity int32 `json:"popularity"`
PopularityRankLeft int8 `json:"popularityRankLeft"`
PopularityRankCenter int8 `json:"popularityRankCenter"`
PopularityRankRight int8 `json:"popularityRankRight"`
GateInPopularity int8 `json:"gateInPopularity"`
Rarity CardRarity `json:"rarity"`
TrainerName string `json:"trainerName"`
IsGhost bool `json:"isGhost"`
IsRunningStyleExInitialized bool `json:"isRunningStyleExInitialized"`
RunningStyleEx RunningStyleEx `json:"runningStyleEx"`
Defeat string `json:"defeat"` // this is an enum, but not worth the effort
RaceDressID int32 `json:"raceDressId"`
RaceDressIDWithOption int32 `json:"raceDressIdWithOption"`
RunningType string `json:"runningType"` // enum not worth
ActiveProperDistance ProperGrade `json:"activeProperDistance"`
ActiveProperGroundType ProperGrade `json:"activeProperGroundType"`
MobID int32 `json:"mobId"`
RaceRecord struct{} `json:"raceRecord"`
FinishOrderRawScore int32 `json:"finishOrderRawScore"`
TrainedCharaData TrainedCharaData `json:"trainedCharaData"`
}
type FactorDataArray struct {
FactorLv int `json:"factorLv"`
FactorID int `json:"factorId"`
BaseFactorID int `json:"baseFactorId"`
UpgradeHistoryList []any `json:"upgradeHistoryList"`
type RunningStyleEx int8
//go:generate go run golang.org/x/tools/cmd/stringer@v0.41.0 -type RunningStyleEx -linecomment
const (
StyleExNone RunningStyleEx = iota // NONE
StyleExRunaway // Oonige
)
func (r RunningStyleEx) AppendText(b []byte) ([]byte, error) {
return append(b, r.String()...), nil
}
type FavoriteData struct {
func (r RunningStyleEx) MarshalText() ([]byte, error) {
return r.AppendText(nil)
}
func (r *RunningStyleEx) UnmarshalText(b []byte) error {
switch string(b) {
case "NONE":
*r = StyleExNone
case "Oonige":
*r = StyleExRunaway
default:
*r = 0
return fmt.Errorf("unknown extra running style %q", b)
}
return nil
}
type ProperGrade int8
//go:generate go run golang.org/x/tools/cmd/stringer@v0.41.0 -type ProperGrade -trimprefix Aptitude
const (
AptitudeNull ProperGrade = iota
AptitudeG
AptitudeF
AptitudeE
AptitudeD
AptitudeC
AptitudeB
AptitudeA
AptitudeS
)
func (r ProperGrade) AppendText(b []byte) ([]byte, error) {
return append(b, r.String()...), nil
}
func (r ProperGrade) MarshalText() ([]byte, error) {
return r.AppendText(nil)
}
func (r *ProperGrade) UnmarshalText(b []byte) error {
switch string(b) {
case "Null":
*r = AptitudeNull
case "G":
*r = AptitudeG
case "F":
*r = AptitudeF
case "E":
*r = AptitudeE
case "D":
*r = AptitudeD
case "C":
*r = AptitudeC
case "B":
*r = AptitudeB
case "A":
*r = AptitudeA
case "S":
*r = AptitudeS
default:
*r = 0
return fmt.Errorf("unknown motivation %q", b)
}
return nil
}
type TrainedCharaData struct {
ID int32 `json:"id"`
IsSaved bool `json:"isSaved"`
ViewerID int64 `json:"viewerId"`
OwnerViewerID int64 `json:"ownerViewerId"`
OwnerTrainedCharaID int32 `json:"ownerTrainedCharaId"`
UseType int32 `json:"useType"`
CardID horse.UmaID `json:"cardId"`
NickNameID int32 `json:"nickNameId"`
NickNameIDArray []int32 `json:"nickNameIdArray"`
Stamina int32 `json:"stamina"`
Speed int32 `json:"speed"`
Power int32 `json:"power"`
Guts int32 `json:"guts"`
Wiz int32 `json:"wiz"`
Fans int32 `json:"fans"`
Rank int32 `json:"rank"`
RankScore int32 `json:"rankScore"`
RunningStyle int32 `json:"runningStyle"`
ProperGroundTurf int32 `json:"properGroundTurf"`
ProperGroundDirt int32 `json:"properGroundDirt"`
ProperDistanceShort int32 `json:"properDistanceShort"`
ProperDistanceMile int32 `json:"properDistanceMile"`
ProperDistanceMiddle int32 `json:"properDistanceMiddle"`
ProperDistanceLong int32 `json:"properDistanceLong"`
ProperRunningStyleNige int32 `json:"properRunningStyleNige"`
ProperRunningStyleSenko int32 `json:"properRunningStyleSenko"`
ProperRunningStyleSashi int32 `json:"properRunningStyleSashi"`
ProperRunningStyleOikomi int32 `json:"properRunningStyleOikomi"`
SuccessionCount int32 `json:"successionCount"`
FactorDataArray []FactorData `json:"factorDataArray"`
CreateTime string `json:"createTime"`
ScenarioID horse.ScenarioID `json:"scenarioId"`
TalentLevel int32 `json:"talentLevel"`
CharaGrade int32 `json:"charaGrade"`
Rarity int32 `json:"rarity"`
IsLock bool `json:"isLock"`
FavoriteData struct{} `json:"favoriteData"`
CachedCreateTimeTimeStamp int64 `json:"cachedCreateTimeTimeStamp"`
SortedFactorList []umadump.Never `json:"sortedFactorList"`
SortedFactorProfileCardList []umadump.Never `json:"sortedFactorProfileCardList"`
FactorListIncludingSuccession []umadump.Never `json:"factorListIncludingSuccession"`
SuccessionCharaList []SuccessionCharaList `json:"successionCharaList"`
IsSuccessionHistoryInitialized bool `json:"isSuccessionHistoryInitialized"`
SuccessionHistoryList []umadump.Never `json:"successionHistoryList"`
AcquiredSkillArray []AcquiredSkill `json:"acquiredSkillArray"`
SupportCardArray []SupportCard `json:"supportCardArray"`
SingleModeRaceResultArray []SingleModeRaceResult `json:"singleModeRaceResultArray"`
WinSaddleArray []umadump.Never `json:"winSaddleArray"`
WinSaddleIDArray []horse.SaddleID `json:"winSaddleIdArray"`
CacheCharaID int32 `json:"cacheCharaId"`
MasterCardData struct{} `json:"masterCardData"`
MasterCharaData struct{} `json:"masterCharaData"`
MasterCardRarityData struct{} `json:"masterCardRarityData"`
SingleTotalRaceNum int32 `json:"singleTotalRaceNum"`
SingleWinNum int32 `json:"singleWinNum"`
TrainedCharaDataAccessor struct{} `json:"trainedCharaDataAccessor"`
}
type FactorData struct {
FactorLv int32 `json:"factorLv"`
FactorID horse.SparkID `json:"factorId"`
BaseFactorID horse.SparkID `json:"baseFactorId"`
UpgradeHistoryList []umadump.Never `json:"upgradeHistoryList"`
}
type SuccessionCharaList struct {
PositionID int `json:"positionId"`
CardID int `json:"cardId"`
Rarity int `json:"rarity"`
Level int `json:"level"`
Rank int `json:"rank"`
FactorDataArray []FactorDataArray `json:"factorDataArray"`
SortedFactorList []any `json:"sortedFactorList"`
SortedFactorListForProfileCard []any `json:"sortedFactorListForProfileCard"`
OwnerViewerID int `json:"ownerViewerId"`
IsPlayer bool `json:"isPlayer"`
WinSaddleArray []any `json:"winSaddleArray"`
WinSaddleIDArray []int `json:"winSaddleIdArray"`
PositionID int32 `json:"positionId"`
CardID int32 `json:"cardId"`
Rarity int32 `json:"rarity"`
Level int32 `json:"level"`
Rank int32 `json:"rank"`
FactorDataArray []FactorData `json:"factorDataArray"`
SortedFactorList []umadump.Never `json:"sortedFactorList"`
SortedFactorListForProfileCard []umadump.Never `json:"sortedFactorListForProfileCard"`
OwnerViewerID int64 `json:"ownerViewerId"`
IsPlayer bool `json:"isPlayer"`
WinSaddleArray []umadump.Never `json:"winSaddleArray"`
WinSaddleIDArray []int32 `json:"winSaddleIdArray"`
}
type Master struct {
type AcquiredSkill struct {
MasterID horse.SkillID `json:"masterId"`
Level int `json:"level"`
Master struct{} `json:"master"`
}
type AcquiredSkillArray struct {
MasterID int `json:"masterId"`
Level int `json:"level"`
Master Master `json:"master"`
type SupportCard struct {
Position int32 `json:"position"`
SupportCardID horse.SupportCardID `json:"supportCardId"`
LimitBreakCount int32 `json:"limitBreakCount"`
Exp int32 `json:"exp"`
}
type SupportCardArray struct {
Position int `json:"position"`
SupportCardID int `json:"supportCardId"`
LimitBreakCount int `json:"limitBreakCount"`
Exp int `json:"exp"`
}
type SingleModeRaceResultArray struct {
type SingleModeRaceResult struct {
Turn int `json:"turn"`
ProgramID int `json:"programId"`
RaceInstanceID int `json:"raceInstanceId"`
@@ -664,172 +877,46 @@ type SingleModeRaceResultArray struct {
ResultRank int `json:"resultRank"`
ScenarioID int `json:"scenarioId"`
}
type MasterCardData struct {
}
type MasterCharaData struct {
}
type MasterCardRarityData struct {
}
type TrainedCharaDataAccessor struct {
}
type TrainedCharaData struct {
ID int `json:"id"`
IsSaved bool `json:"isSaved"`
ViewerID int64 `json:"viewerId"`
OwnerViewerID int `json:"ownerViewerId"`
OwnerTrainedCharaID int `json:"ownerTrainedCharaId"`
UseType int `json:"useType"`
CardID int `json:"cardId"`
NickNameID int `json:"nickNameId"`
NickNameIDArray []int `json:"nickNameIdArray"`
Stamina int `json:"stamina"`
Speed int `json:"speed"`
Power int `json:"power"`
Guts int `json:"guts"`
Wiz int `json:"wiz"`
Fans int `json:"fans"`
Rank int `json:"rank"`
RankScore int `json:"rankScore"`
RunningStyle int `json:"runningStyle"`
ProperGroundTurf int `json:"properGroundTurf"`
ProperGroundDirt int `json:"properGroundDirt"`
ProperDistanceShort int `json:"properDistanceShort"`
ProperDistanceMile int `json:"properDistanceMile"`
ProperDistanceMiddle int `json:"properDistanceMiddle"`
ProperDistanceLong int `json:"properDistanceLong"`
ProperRunningStyleNige int `json:"properRunningStyleNige"`
ProperRunningStyleSenko int `json:"properRunningStyleSenko"`
ProperRunningStyleSashi int `json:"properRunningStyleSashi"`
ProperRunningStyleOikomi int `json:"properRunningStyleOikomi"`
SuccessionCount int `json:"successionCount"`
FactorDataArray []FactorDataArray `json:"factorDataArray"`
CreateTime string `json:"createTime"`
ScenarioID int `json:"scenarioId"`
TalentLevel int `json:"talentLevel"`
CharaGrade int `json:"charaGrade"`
Rarity int `json:"rarity"`
IsLock bool `json:"isLock"`
FavoriteData FavoriteData `json:"favoriteData"`
CachedCreateTimeTimeStamp int `json:"cachedCreateTimeTimeStamp"`
SortedFactorList []any `json:"sortedFactorList"`
SortedFactorProfileCardList []any `json:"sortedFactorProfileCardList"`
FactorListIncludingSuccession []any `json:"factorListIncludingSuccession"`
SuccessionCharaList []SuccessionCharaList `json:"successionCharaList"`
IsSuccessionHistoryInitialized bool `json:"isSuccessionHistoryInitialized"`
SuccessionHistoryList []any `json:"successionHistoryList"`
AcquiredSkillArray []AcquiredSkillArray `json:"acquiredSkillArray"`
SupportCardArray []SupportCardArray `json:"supportCardArray"`
SingleModeRaceResultArray []SingleModeRaceResultArray `json:"singleModeRaceResultArray"`
WinSaddleArray []any `json:"winSaddleArray"`
WinSaddleIDArray []int `json:"winSaddleIdArray"`
CacheCharaID int `json:"cacheCharaId"`
MasterCardData MasterCardData `json:"masterCardData"`
MasterCharaData MasterCharaData `json:"masterCharaData"`
MasterCardRarityData MasterCardRarityData `json:"masterCardRarityData"`
SingleTotalRaceNum int `json:"singleTotalRaceNum"`
SingleWinNum int `json:"singleWinNum"`
TrainedCharaDataAccessor TrainedCharaDataAccessor `json:"trainedCharaDataAccessor"`
}
type PlayerTeamMember struct {
HorseIndex int `json:"horseIndex"`
PostNumber int `json:"postNumber"`
CharaID int `json:"charaId"`
CharaName string `json:"charaName"`
FinishOrder int `json:"finishOrder"`
FinishTimeRaw float32 `json:"finishTimeRaw"`
FinishTimeScaled float32 `json:"finishTimeScaled"`
FinishDiffTimeFromPrev float32 `json:"finishDiffTimeFromPrev"`
RaceParam RaceParam `json:"raceParam"`
ResponseHorseData ResponseHorseData `json:"responseHorseData"`
Popularity int `json:"popularity"`
PopularityRankLeft int `json:"popularityRankLeft"`
PopularityRankCenter int `json:"popularityRankCenter"`
PopularityRankRight int `json:"popularityRankRight"`
GateInPopularity int `json:"gateInPopularity"`
Rarity string `json:"rarity"`
TrainerName string `json:"trainerName"`
IsGhost bool `json:"isGhost"`
IsRunningStyleExInitialized bool `json:"isRunningStyleExInitialized"`
RunningStyleEx string `json:"runningStyleEx"`
Defeat string `json:"defeat"`
RaceDressID int `json:"raceDressId"`
RaceDressIDWithOption int `json:"raceDressIdWithOption"`
RunningType string `json:"runningType"`
ActiveProperDistance string `json:"activeProperDistance"`
ActiveProperGroundType string `json:"activeProperGroundType"`
MobID int `json:"mobId"`
RaceRecord RaceRecord `json:"raceRecord"`
FinishOrderRawScore int `json:"finishOrderRawScore"`
TrainedCharaData TrainedCharaData `json:"trainedCharaData"`
}
type PlayerTeamTopFinishOrderHorse struct {
HorseIndex int `json:"horseIndex"`
PostNumber int `json:"postNumber"`
CharaID int `json:"charaId"`
CharaName string `json:"charaName"`
FinishOrder int `json:"finishOrder"`
FinishTimeRaw float32 `json:"finishTimeRaw"`
FinishTimeScaled float32 `json:"finishTimeScaled"`
FinishDiffTimeFromPrev float32 `json:"finishDiffTimeFromPrev"`
RaceParam RaceParam `json:"raceParam"`
ResponseHorseData ResponseHorseData `json:"responseHorseData"`
Popularity int `json:"popularity"`
PopularityRankLeft int `json:"popularityRankLeft"`
PopularityRankCenter int `json:"popularityRankCenter"`
PopularityRankRight int `json:"popularityRankRight"`
GateInPopularity int `json:"gateInPopularity"`
Rarity string `json:"rarity"`
TrainerName string `json:"trainerName"`
IsGhost bool `json:"isGhost"`
IsRunningStyleExInitialized bool `json:"isRunningStyleExInitialized"`
RunningStyleEx string `json:"runningStyleEx"`
Defeat string `json:"defeat"`
RaceDressID int `json:"raceDressId"`
RaceDressIDWithOption int `json:"raceDressIdWithOption"`
RunningType string `json:"runningType"`
ActiveProperDistance string `json:"activeProperDistance"`
ActiveProperGroundType string `json:"activeProperGroundType"`
MobID int `json:"mobId"`
RaceRecord RaceRecord `json:"raceRecord"`
FinishOrderRawScore int `json:"finishOrderRawScore"`
TrainedCharaData TrainedCharaData `json:"trainedCharaData"`
type CardRarity int8
//go:generate go run golang.org/x/tools/cmd/stringer@v0.41.0 -type CardRarity -linecomment -trimprefix Card
const (
CardNone CardRarity = iota // NONE
CardRare1
CardRare2
CardRare3
CardRare4
CardRare5
)
func (r CardRarity) AppendText(b []byte) ([]byte, error) {
return append(b, r.String()...), nil
}
type RaceHorse struct {
HorseIndex int `json:"horseIndex"`
PostNumber int `json:"postNumber"`
CharaID int `json:"charaId"`
CharaName string `json:"charaName"`
FinishOrder int `json:"finishOrder"`
FinishTimeRaw float32 `json:"finishTimeRaw"`
FinishTimeScaled float32 `json:"finishTimeScaled"`
FinishDiffTimeFromPrev float32 `json:"finishDiffTimeFromPrev"`
RaceParam RaceParam `json:"raceParam"`
ResponseHorseData ResponseHorseData `json:"responseHorseData"`
Popularity int `json:"popularity"`
PopularityRankLeft int `json:"popularityRankLeft"`
PopularityRankCenter int `json:"popularityRankCenter"`
PopularityRankRight int `json:"popularityRankRight"`
GateInPopularity int `json:"gateInPopularity"`
Rarity string `json:"rarity"`
TrainerName string `json:"trainerName"`
IsGhost bool `json:"isGhost"`
IsRunningStyleExInitialized bool `json:"isRunningStyleExInitialized"`
RunningStyleEx string `json:"runningStyleEx"`
Defeat string `json:"defeat"`
RaceDressID int `json:"raceDressId"`
RaceDressIDWithOption int `json:"raceDressIdWithOption"`
RunningType string `json:"runningType"`
ActiveProperDistance string `json:"activeProperDistance"`
ActiveProperGroundType string `json:"activeProperGroundType"`
MobID int `json:"mobId"`
RaceRecord RaceRecord `json:"raceRecord"`
FinishOrderRawScore int `json:"finishOrderRawScore"`
TrainedCharaData TrainedCharaData `json:"trainedCharaData,omitempty"`
func (r CardRarity) MarshalText() ([]byte, error) {
return r.AppendText(nil)
}
type RaceRewardSingle struct {
// TODO(zeph): this
func (r *CardRarity) UnmarshalText(b []byte) error {
switch string(b) {
case "NONE":
*r = CardNone
case "Rare1":
*r = CardRare1
case "Rare2":
*r = CardRare2
case "Rare3":
*r = CardRare3
case "Rare4":
*r = CardRare4
case "Rare5":
*r = CardRare5
default:
*r = 0
return fmt.Errorf("unknown card rarity %q", b)
}
return nil
}
type CharaGradeType int8
+182
View File
@@ -0,0 +1,182 @@
package replay_test
import (
_ "embed"
"encoding/json"
"strings"
"testing"
"git.sunturtle.xyz/zephyr/horse/umadump/replay"
)
//go:embed Mihono_Bourbon-74.2421s-20260829.json
var testRace string
func TestRaceUnmarshal(t *testing.T) {
// A replay is simply too large to bother converting by hand for a unit test.
// Just test that we can unmarshal and certain important fields are populated.
d := json.NewDecoder(strings.NewReader(testRace))
d.DisallowUnknownFields()
var got replay.Race
if err := d.Decode(&got); err != nil {
t.Error(err)
}
if got.RaceType != replay.RaceTypeChampions {
t.Errorf("wrong race type: want %v, got %v", replay.RaceTypeChampions, got.RaceType)
}
if got.RandomSeed != 1667907263 {
t.Errorf("wrong random seed: want 1667907263, got %d", got.RandomSeed)
}
if got.RaceCourseSet.ID != 10903 {
t.Errorf("wrong race course: want 10903, got %d", got.RaceCourseSet.ID)
}
if got.GroundCondition != replay.GroundFirm {
t.Errorf("wrong ground condition: want %v, got %v", replay.GroundFirm, got.GroundCondition)
}
if got.Weather != replay.WeatherCloudy {
t.Errorf("wrong weather: want %v, got %v", replay.WeatherCloudy, got.Weather)
}
if got.NumRaceHorses != 9 {
t.Errorf("wrong number of race horses: want 9, got %d", got.NumRaceHorses)
}
if len(got.PlayerTeamMemberArray) != 3 {
t.Errorf("wrong number of team members: want 3, got %d", len(got.PlayerTeamMemberArray))
}
for _, m := range got.PlayerTeamMemberArray {
if m.CharaID == 0 {
t.Error("missing team member chara id")
}
if m.FinishTimeRaw == 0 {
t.Error("missing team member finish time raw")
}
if m.RaceParam.RawSpeed == 0 {
t.Error("missing team member race param raw speed")
}
if m.RaceParam.BaseSpeed == 0 {
t.Error("missing team member race param base speed")
}
if m.ResponseHorseData.ViewerID == 0 {
t.Error("missing team member response horse data viewer id")
}
if m.ResponseHorseData.TrainedCharaID == 0 {
t.Error("missing team member response horse data trained chara id")
}
if m.ResponseHorseData.CardID == 0 {
t.Error("missing team member response horse data card id")
}
if len(m.ResponseHorseData.SkillArray) == 0 {
t.Error("missing team member response horse data skill array")
}
if m.ResponseHorseData.Speed == 0 {
t.Error("missing team member response horse data speed")
}
if m.ResponseHorseData.RunningStyle == 0 {
t.Error("missing team member response horse data running style")
}
if m.ActiveProperDistance == 0 {
t.Error("missing team member active distance aptitude")
}
if m.ActiveProperGroundType == 0 {
t.Error("missing team member active surface aptitude")
}
if m.TrainedCharaData.ID == 0 {
t.Error("missing team member trained chara id")
}
if m.TrainedCharaData.CardID == 0 {
t.Error("missing team member trained chara card id")
}
if m.TrainedCharaData.Speed == 0 {
t.Error("missing team member trained chara speed")
}
if m.TrainedCharaData.RunningStyle == 0 {
t.Error("missing team member trained chara running style")
}
if m.TrainedCharaData.CreateTime == "" {
t.Error("missing team member trained chara create time")
}
if len(m.TrainedCharaData.AcquiredSkillArray) == 0 {
t.Error("missing team member trained chara acquired skill array")
}
}
if len(got.RaceHorse) != int(got.NumRaceHorses) {
t.Errorf("wrong number of race horses: want %d, got %d", got.NumRaceHorses, len(got.RaceHorse))
}
zeroFinish, zeroDelta := 0, 0
for _, m := range got.RaceHorse {
if m.CharaID == 0 {
t.Error("missing race horse chara id")
}
if m.FinishOrder == 0 {
zeroFinish++
}
if m.FinishTimeRaw == 0 {
t.Error("missing race horse finish time raw")
}
if m.FinishDiffTimeFromPrev == 0 {
zeroDelta++
}
if m.RaceParam.RawSpeed == 0 {
t.Error("missing race horse race param raw speed")
}
if m.RaceParam.BaseSpeed == 0 {
t.Error("missing race horse race param base speed")
}
if m.ResponseHorseData.ViewerID == 0 {
t.Error("missing race horse response horse data viewer id")
}
if m.ResponseHorseData.TrainedCharaID == 0 {
t.Error("missing race horse response horse data trained chara id")
}
if m.ResponseHorseData.CardID == 0 {
t.Error("missing race horse response horse data card id")
}
if len(m.ResponseHorseData.SkillArray) == 0 {
t.Error("missing race horse response horse data skill array")
}
if m.ResponseHorseData.Speed == 0 {
t.Error("missing race horse response horse data speed")
}
if m.ResponseHorseData.RunningStyle == 0 {
t.Error("missing race horse response horse data running style")
}
if m.ActiveProperDistance == 0 {
t.Error("missing race horse active distance aptitude")
}
if m.ActiveProperGroundType == 0 {
t.Error("missing race horse active surface aptitude")
}
if m.TrainedCharaData.ID == 0 {
t.Error("missing race horse trained chara id")
}
if m.TrainedCharaData.CardID == 0 {
t.Error("missing race horse trained chara card id")
}
if m.TrainedCharaData.Speed == 0 {
t.Error("missing race horse trained chara speed")
}
if m.TrainedCharaData.RunningStyle == 0 {
t.Error("missing race horse trained chara running style")
}
if m.TrainedCharaData.CreateTime == "" {
t.Error("missing race horse trained chara create time")
}
if len(m.TrainedCharaData.AcquiredSkillArray) == 0 {
t.Error("missing race horse trained chara acquired skill array")
}
}
if zeroFinish != 1 {
t.Errorf("%d horses in index 0 finish", zeroFinish)
}
if zeroDelta != 1 {
t.Errorf("%d horses with zero finish time delta", zeroDelta)
}
if got.SimDataBase64 != encodedSimdata {
t.Errorf("wrong simdata:\nwant %s\n got %s", encodedSimdata, got.SimDataBase64)
}
if len(got.HorseIndexByFinishOrder) != len(got.RaceHorse) {
t.Errorf("length of horseIndexByFinishOrder (%d) doesn't match length of raceHorse (%d)", len(got.HorseIndexByFinishOrder), len(got.RaceHorse))
}
if len(got.HorseIndexByPopularity) != len(got.RaceHorse) {
t.Errorf("length of horseIndexByPopularity (%d) doesn't match length of raceHorse (%d)", len(got.HorseIndexByPopularity), len(got.RaceHorse))
}
}
+25
View File
@@ -0,0 +1,25 @@
// Code generated by "stringer -type RunningStyleEx -linecomment"; DO NOT EDIT.
package replay
import "strconv"
func _() {
// An "invalid array index" compiler error signifies that the constant values have changed.
// Re-run the stringer command to generate them again.
var x [1]struct{}
_ = x[StyleExNone-0]
_ = x[StyleExRunaway-1]
}
const _RunningStyleEx_name = "NONEOonige"
var _RunningStyleEx_index = [...]uint8{0, 4, 10}
func (i RunningStyleEx) String() string {
idx := int(i) - 0
if i < 0 || idx >= len(_RunningStyleEx_index)-1 {
return "RunningStyleEx(" + strconv.FormatInt(int64(i), 10) + ")"
}
return _RunningStyleEx_name[_RunningStyleEx_index[idx]:_RunningStyleEx_index[idx+1]]
}
File diff suppressed because one or more lines are too long
+20
View File
@@ -0,0 +1,20 @@
<script lang="ts">
interface Props {
value: number;
}
let { value = $bindable() }: Props = $props();
</script>
<div class="flex">
<input
class="my-auto w-full grow justify-self-center-safe"
type="range"
id="raceLen"
min="1000"
max="3600"
step="100"
bind:value
/>
<span class="my-auto flex-none pl-4 text-sm">{value}m</span>
</div>
+1
View File
@@ -20,6 +20,7 @@
skill_id: 0,
name: 'Skill data is loading...',
description: 'Skill data is still loading, or else something went wrong.',
group_skills: [],
group: 0,
rarity: 1,
group_rate: 1,
+4
View File
@@ -4,6 +4,10 @@ import type { RegionalName } from '$lib/regional-name';
* Lobby conversation data.
*/
export interface Conversation {
/**
* Gallery ID.
*/
id: number;
/**
* Character who owns the conversation as a gallery entry.
*/
+16
View File
@@ -14,6 +14,10 @@ export interface Skill {
* Regional skil description.
*/
description: string;
/**
* Other skills in the skill group.
*/
group_skills: GroupSkill[];
/**
* Skill group ID.
*/
@@ -58,6 +62,18 @@ export interface Skill {
icon_id: number;
}
/**
* Skill reference in a skill group.
*/
export interface GroupSkill {
/** Skill ID. */
skill_id: number;
/** Group rate, as in `Skill`. */
group_rate: Skill['group_rate'];
/** Rarity, as in `Skill`. */
rarity: Skill['rarity'];
}
export enum DurationScale {
Direct = 1,
FrontDistance = 2,
+3 -2
View File
@@ -771,16 +771,17 @@ export function conservePowerAccel(
distanceType: Distance,
style: RunningStyle,
rawPower: number,
basePower: number,
powerSkillBonus?: number,
spotStruggled?: boolean,
rushed?: boolean,
): number {
const power = rawPower + (powerSkillBonus ?? 0);
const power = basePower + 0.5 * Math.max(rawPower - 1200, 0) + (powerSkillBonus ?? 0);
if (power <= 1200) {
return 0;
}
const sdc = conserveStratDistCoef[style][distanceType];
const activity = rushed ? 0.8 : spotStruggled ? 0.98 : 1;
const activity = (rushed ? 0.8 : 1) * (spotStruggled ? 0.98 : 1);
return Math.sqrt((power - 1200) * 130) * 0.001 * sdc * activity;
}
+14
View File
@@ -0,0 +1,14 @@
import * as z from 'zod';
const Entry = z.strictObject({
home_story_trigger_id: z.int32(),
new_flag: z.literal(0).or(z.literal(1)),
});
export type Entry = z.infer<typeof Entry>;
export const Gallery = z.object({
talk_gallery_list: z.array(Entry),
});
export type Gallery = z.infer<typeof Gallery>;
+57 -8
View File
@@ -9,25 +9,51 @@
requiredCharas,
convoCharas,
} from '$lib/data/convo';
import * as talk from '$lib/umadump/talk';
import CharaPick from '$lib/CharaPick.svelte';
import { onMount } from 'svelte';
import CharaIcon from '$lib/CharaIcon.svelte';
import { SvelteSet } from 'svelte/reactivity';
let conversations: Conversation[] = $state([]);
const convoMap = $derived(byChara(conversations));
const popular = $derived(groupPopulars(conversations));
let allConvos: Conversation[] = $state([]);
const popular = $derived(groupPopulars(allConvos));
let characters: Character[] = $state([]);
const selCharas = $derived(characters.filter((c) => convoMap.has(c.chara_id)));
const names = $derived(charaNames(characters));
const minSuggest = 8;
onMount(async () => {
[conversations, characters] = await Promise.all([conversation(), character()]);
[allConvos, characters] = await Promise.all([conversation(), character()]);
});
let chara: Character | undefined = $state();
let convo = $state(1);
let talkFiles: FileList | undefined = $state();
let readIDs = new SvelteSet<number>();
let talkFileInvalid = $state(false);
async function setRead(f: FileList) {
talkFiles = f;
readIDs.clear();
talkFileInvalid = false;
if (talkFiles == null || talkFiles.length === 0) {
return;
}
const t = await talkFiles[0].text();
const d = JSON.parse(t);
const g = talk.Gallery.safeDecode(d);
if (!g.success) {
talkFileInvalid = true;
console.warn(g.error);
return;
}
for (const e of g.data.talk_gallery_list) {
readIDs.add(e.home_story_trigger_id);
}
}
const conversations = $derived(allConvos.filter((c) => !readIDs.has(c.id)));
const convoMap = $derived(byChara(conversations));
const selCharas = $derived(characters.filter((c) => convoMap.has(c.chara_id)));
const options = $derived(convoMap.get(chara?.chara_id ?? 0) ?? []);
const cur = $derived(options.find((c) => c.number === convo));
const charaIDs = $derived(cur != null ? convoCharas(cur) : []);
@@ -47,12 +73,12 @@
</script>
<h1 class="text-4xl">Lobby Conversations</h1>
<div class="mx-auto mt-8 flex flex-col rounded-md text-center shadow-md ring md:max-w-xl md:flex-row">
<div class="m-4 flex-1 md:mt-3">
<div class="mx-auto mt-8 grid grid-cols-1 gap-4 rounded-md p-4 text-center shadow-md ring md:max-w-xl md:grid-cols-6">
<div class="md:col-span-3">
<label for="chara" class="hidden md:inline">Character</label>
<CharaPick id="chara" characters={selCharas} class="w-full" bind:value={chara} required />
</div>
<div class="m-4 flex-1 md:mt-3">
<div class="md:col-span-3">
<label for="convo" class="hidden md:inline">Conversation</label>
<select id="convo" bind:value={convo} class="w-full">
{#each options as opt (opt.number)}
@@ -60,6 +86,29 @@
{/each}
</select>
</div>
<div class="col-span-full">
<label for="talkgallery" class="block">
Select
<a href="https://werseter.github.io/umadump/" target="_blank" rel="noopener noreferrer">
<span class="inline-block font-mono">talk_gallery_data.json</span>
</a>
to hide read conversations.
</label>
<input
type="file"
class="rounded-full p-4"
accept="application/json"
id="talkgallery"
bind:files={() => talkFiles ?? new DataTransfer().files, setRead}
/>
<button
class="ml-4 rounded-full bg-mist-300 p-2 hover:cursor-pointer dark:bg-mist-900"
onclick={() => setRead(new DataTransfer().files)}>Reset</button
>
</div>
{#if talkFileInvalid}
<div class="col-span-full">Invalid File</div>
{/if}
</div>
{#if cur}
<div class="shadow-sm transition-shadow hover:shadow-md">
+11 -29
View File
@@ -26,6 +26,7 @@
import Article from '../Article.svelte';
import Sec from '../Sec.svelte';
import * as skill from '$lib/data/skill';
import RaceLenSlider from '$lib/RaceLenSlider.svelte';
let raceLen = $state(2000);
let secSpeedStyle = $state(RunningStyle.FrontRunner);
@@ -224,10 +225,10 @@
},
]);
const fcSeries: ComputedSeries[] = $derived([
{ label: 'Front Runner', y: (x) => conservePowerAccel(distance(raceLen), RunningStyle.FrontRunner, x) },
{ label: 'Pace Chaser', y: (x) => conservePowerAccel(distance(raceLen), RunningStyle.PaceChaser, x) },
{ label: 'Late Surger', y: (x) => conservePowerAccel(distance(raceLen), RunningStyle.LateSurger, x) },
{ label: 'End Closer', y: (x) => conservePowerAccel(distance(raceLen), RunningStyle.EndCloser, x) },
{ label: 'Front Runner', y: (x, raw) => conservePowerAccel(distance(raceLen), RunningStyle.FrontRunner, raw, x) },
{ label: 'Pace Chaser', y: (x, raw) => conservePowerAccel(distance(raceLen), RunningStyle.PaceChaser, raw, x) },
{ label: 'Late Surger', y: (x, raw) => conservePowerAccel(distance(raceLen), RunningStyle.LateSurger, raw, x) },
{ label: 'End Closer', y: (x, raw) => conservePowerAccel(distance(raceLen), RunningStyle.EndCloser, raw, x) },
]);
</script>
@@ -514,23 +515,13 @@
plotOptions={{ color: { legend: true } }}
/>
<div class="mx-auto flex w-full md:max-w-2xl">
<label class="my-auto hidden flex-none pr-2 text-right text-sm md:inline" for="secSpeedRaceLen">Race Length</label>
<input
class="my-auto max-w-40 flex-1 md:flex-2"
type="range"
id="secSpeedRaceLen"
min="1000"
max="3600"
step="100"
bind:value={raceLen}
/>
<span class="my-auto flex-none pl-2 text-sm">{raceLen}m</span>
<label class="my-auto mr-2 hidden flex-none pr-2 text-right text-sm md:inline" for="secSpeedRaceLen">Race Length</label>
<RaceLenSlider bind:value={raceLen} />
</div>
</div>
<p>
The exact mechanics of conserving power are unclear at this time. What has been confirmed is that rushed and <a
href="#spot-struggle">spot struggle</a
> both reduce the chance for it to fire.
Rushed and <a href="#spot-struggle">spot struggle</a> reduce both the duration and effectiveness of full charge, potentially to
zero. In practice, this appears to be a concern primarily on sprints, which already have a -55% penalty to full charge duration.
</p>
<Sec h={3} id="lane-combo">Lane Combo</Sec>
@@ -670,17 +661,8 @@
plotOptions={{ color: { legend: true } }}
/>
<div class="mx-auto flex w-full md:max-w-2xl">
<label class="my-auto hidden flex-1 pr-2 text-right text-sm md:inline" for="secSpeedRaceLen">Race Length</label>
<input
class="my-auto max-w-40 flex-1 md:flex-2"
type="range"
id="secSpeedRaceLen"
min="1000"
max="3600"
step="100"
bind:value={raceLen}
/>
<span class="my-auto flex-1 pl-2 text-sm">{raceLen}m</span>
<label class="my-auto mr-2 hidden flex-1 pr-2 text-right text-sm md:inline" for="secSpeedRaceLen">Race Length</label>
<RaceLenSlider bind:value={raceLen} />
<label class="my-auto hidden flex-1 pr-2 text-right text-sm md:inline" for="secSpeedStyle">Style</label>
<select class="my-auto flex-1 text-sm" id="secSpeedStyle" bind:value={secSpeedStyle}>
{#each RUNNING_STYLES as [name, style] (style)}
+16 -17
View File
@@ -5,6 +5,7 @@
import * as race from '$lib/race';
import StatChart from '$lib/StatChart.svelte';
import { resolve } from '$app/paths';
import RaceLenSlider from '$lib/RaceLenSlider.svelte';
function modemean(x: number, bonus: number, p: number): number {
return x * (1 - p) + x * bonus * p;
@@ -58,10 +59,10 @@
]);
const laneChange: ComputedSeries[] = [{ label: 'Lane Change Target Speed', y: (x) => race.laneChangeSpeed(x) }];
const conservePower: Array<ComputedSeries | null> = $derived([
{ label: 'Conserved Power', y: (_raw, x) => race.conservePowerAccel(distanceType, style, x, 0, false, false) },
{ label: 'Rushed', y: (_raw, x) => race.conservePowerAccel(distanceType, style, x, 0, false, true) },
{ label: 'Conserved Power', y: (x, raw) => race.conservePowerAccel(distanceType, style, raw, x, 0, false, false) },
{ label: 'Rushed', y: (x, raw) => race.conservePowerAccel(distanceType, style, raw, x, 0, false, true) },
styleIsFront
? { label: 'Spot Struggled', y: (_raw, x) => race.conservePowerAccel(distanceType, style, x, 0, true, false) }
? { label: 'Spot Struggled', y: (x, raw) => race.conservePowerAccel(distanceType, style, raw, x, 0, true, false) }
: null,
]);
const gutsSpurt: Array<ComputedSeries | null> = $derived([
@@ -174,16 +175,9 @@
/>
<div class="mx-auto grid w-full grid-cols-3 gap-y-6 md:max-w-2xl md:grid-cols-7">
{#if pick?.len}
<input
class="col-span-2 my-auto w-full justify-self-center-safe"
type="range"
id="raceLen"
min="1000"
max="3600"
step="100"
bind:value={raceLen}
/>
<span class="col-span-1 my-auto pl-2 text-sm">{raceLen}m</span>
<div class="col-span-3 my-auto mr-4">
<RaceLenSlider bind:value={raceLen} />
</div>
{/if}
{#if pick?.style}
<select class="col-span-1 my-auto text-sm md:col-span-2" id="stylePick" bind:value={style}>
@@ -329,11 +323,16 @@
Bonus acceleration at spurt start from the Charge Up/Fully Charged mechanic. Scales with base power plus passive skills, so
modifiers for ground conditions and career mode do not apply.
</p>
<p>
This mechanic does not appear to be well understood, in particular how long the acceleration bonus lasts and how the rushed
and spot struggle multipliers apply.
</p>
{@render statChart(race.Stat.Power, conservePower, 'Acceleration (m/s²)', [0, 0.35], { len: true, style: true })}
<p>
The duration of Fully Charged does not depend directly on stats, but rather on a hidden <i>conserved power</i> value which
increases per horse throughout the race. Conserved power does not increase while <a href="#spot-struggle">spot struggling</a>,
so for front runners, the duration may scale negatively with guts. It also does not increase while running in any
<a href="#poskeep">position keep mode</a>
other than pace-down, so its duration may have a very slight negative correlation with wit.
<a href="#rushed">Rushed</a> additionally prevents accumulating conserved power, so full charge duration may scale positively with
wit amortized across races. Otherwise, power conservation rate depends on running style.
</p>
<Sec h={2} id="guts">Guts</Sec>
+4 -10
View File
@@ -4,6 +4,7 @@
import * as accel from '$lib/racelib/accel';
import type { Attachment } from 'svelte/attachments';
import CalcInfo from '../CalcInfo.svelte';
import RaceLenSlider from '$lib/RaceLenSlider.svelte';
let rawSpeed = $state(1200);
let rawPower = $state(1200);
@@ -231,16 +232,9 @@
</div>
<div class="mb-auto grid max-w-4xl grid-cols-6 rounded-md text-center shadow-md ring md:col-span-6">
<input
class="col-span-4 m-4 w-full justify-self-center-safe"
type="range"
id="raceLen"
min="1000"
max="3600"
step="100"
bind:value={raceLen}
/>
<span class="col-span-2 my-auto mb-4 pl-2 text-sm">{raceLen}m</span>
<div class="col-span-full m-4">
<RaceLenSlider bind:value={raceLen} />
</div>
<select id="surface" bind:value={surface} class="col-span-2 mx-2 mb-4">
<option value={race.Surface.Turf}>Turf</option>
<option value={race.Surface.Dirt}>Dirt</option>
+4 -10
View File
@@ -4,6 +4,7 @@
import * as accel from '$lib/racelib/accel';
import type { Attachment } from 'svelte/attachments';
import CalcInfo from '../CalcInfo.svelte';
import RaceLenSlider from '$lib/RaceLenSlider.svelte';
let rawPower = $state(1200);
let rawGuts = $state(1200);
@@ -216,16 +217,9 @@
</div>
</div>
<div class="mb-auto grid max-w-4xl grid-cols-6 rounded-md text-center shadow-md ring md:col-span-3">
<input
class="col-span-4 m-4 w-full justify-self-center-safe"
type="range"
id="raceLen"
min="1000"
max="3600"
step="100"
bind:value={raceLen}
/>
<span class="col-span-2 my-auto mb-4 pl-2 text-sm">{raceLen}m</span>
<div class="col-span-6 mx-4 mt-4">
<RaceLenSlider bind:value={raceLen} />
</div>
<select id="surface" bind:value={surface} class="col-span-2 m-4">
<option value={race.Surface.Turf}>Turf</option>
<option value={race.Surface.Dirt}>Dirt</option>
+2 -2
View File
@@ -18,6 +18,7 @@
import StatChart from '$lib/StatChart.svelte';
import { resolve } from '$app/paths';
import SpeedDur from './SpeedDur.svelte';
import RaceLenSlider from '$lib/RaceLenSlider.svelte';
let rawPower = $state(1200);
let rawGuts = $state(1200);
@@ -112,8 +113,7 @@
</select>
</div>
<div class="m-4 md:col-span-2 md:col-start-2">
<label for="raceLen">Race Distance</label>
<input type="number" id="raceLen" required min="1000" max="3600" step="100" bind:value={raceLen} class="w-full" />
<RaceLenSlider bind:value={raceLen} />
</div>
<div class="m-4 self-center md:col-span-2">
<label for="isRunaway" class="mr-1 align-middle">Runaway</label>
+2 -2
View File
@@ -1,6 +1,7 @@
<script lang="ts">
import type { ComputedSeries } from '$lib/chart';
import * as race from '$lib/race';
import RaceLenSlider from '$lib/RaceLenSlider.svelte';
import StatChart from '$lib/StatChart.svelte';
import CalcInfo from '../CalcInfo.svelte';
@@ -98,8 +99,7 @@
</select>
</div>
<div class="m-4 md:col-start-2">
<label for="raceLen">Race Distance</label>
<input type="number" id="raceLen" required min="1000" max="3600" step="100" bind:value={raceLen} class="w-full" />
<RaceLenSlider bind:value={raceLen} />
</div>
<div class="m-4 self-center">
<label for="isCareer" class="mr-1 align-middle">In Career</label>
+2 -10
View File
@@ -1,6 +1,7 @@
<script lang="ts">
import * as course from '$lib/course';
import * as race from '$lib/race';
import RaceLenSlider from '$lib/RaceLenSlider.svelte';
import * as recovery from '$lib/racelib/recovery';
import HPChart from './HPChart.svelte';
import InfoPanels from './InfoPanels.svelte';
@@ -277,16 +278,7 @@
</div>
<div class="mx-auto my-2 grid max-w-5xl grid-cols-1 gap-4 rounded-md text-center shadow-md ring md:grid-cols-6">
<div class="m-4 flex md:col-span-2">
<input
class="col-span-2 my-auto w-full justify-self-center-safe"
type="range"
id="raceLen"
min="1000"
max="3600"
step="100"
bind:value={raceLen}
/>
<span class="my-auto pl-2 text-sm">{raceLen}m</span>
<RaceLenSlider bind:value={raceLen} />
</div>
<select id="surface" class="m-4" bind:value={surface}>
<option value={race.Surface.Turf}>Turf</option>