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" "sync"
"git.sunturtle.xyz/zephyr/horse" "git.sunturtle.xyz/zephyr/horse"
"git.sunturtle.xyz/zephyr/horse/umadump"
) )
type Race struct { type Race struct {
@@ -46,10 +47,10 @@ type Race struct {
PostNumberMax int32 `json:"postNumberMax"` PostNumberMax int32 `json:"postNumberMax"`
PlayerHorseIndex int32 `json:"playerHorseIndex"` PlayerHorseIndex int32 `json:"playerHorseIndex"`
OverridePlayerHorseIndex int32 `json:"overridePlayerHorseIndex"` OverridePlayerHorseIndex int32 `json:"overridePlayerHorseIndex"`
PlayerTeamMemberArray []RaceHorse `json:"playerTeamMemberArray"` PlayerTeamMemberArray []HorseData `json:"playerTeamMemberArray"`
PlayerTeamTopFinishOrderHorse RaceHorse `json:"playerTeamTopFinishOrderHorse"` PlayerTeamTopFinishOrderHorse HorseData `json:"playerTeamTopFinishOrderHorse"`
IsGateInPopularityInitialized bool `json:"isGateInPopularityInitialized"` IsGateInPopularityInitialized bool `json:"isGateInPopularityInitialized"`
RaceHorse []RaceHorse `json:"raceHorse"` RaceHorse []HorseData `json:"raceHorse"`
RaceBibMaster struct{} `json:"raceBibMaster"` RaceBibMaster struct{} `json:"raceBibMaster"`
RaceMaster struct{} `json:"raceMaster"` RaceMaster struct{} `json:"raceMaster"`
RaceInstanceMaster struct{} `json:"raceInstanceMaster"` RaceInstanceMaster struct{} `json:"raceInstanceMaster"`
@@ -64,7 +65,7 @@ type Race struct {
ReplayCheckInfoLegend struct{} `json:"replayCheckInfoLegend"` ReplayCheckInfoLegend struct{} `json:"replayCheckInfoLegend"`
IsDailyLegendRace bool `json:"isDailyLegendRace"` IsDailyLegendRace bool `json:"isDailyLegendRace"`
ReplayCheckInfoChallengeMatch struct{} `json:"replayCheckInfoChallengeMatch"` ReplayCheckInfoChallengeMatch struct{} `json:"replayCheckInfoChallengeMatch"`
RaceRewardSingle RaceRewardSingle `json:"raceRewardSingle"` RaceRewardSingle struct{} `json:"raceRewardSingle"`
ResultHorseIndex int32 `json:"resultHorseIndex"` ResultHorseIndex int32 `json:"resultHorseIndex"`
PrevGradeType CharaGradeType `json:"prevGradeType"` PrevGradeType CharaGradeType `json:"prevGradeType"`
MainStoryRaceGimmickType MainStoryRaceGimmickType `json:"mainStoryRaceGimmickType"` MainStoryRaceGimmickType MainStoryRaceGimmickType `json:"mainStoryRaceGimmickType"`
@@ -537,122 +538,334 @@ func (r *RaceDifficulty) UnmarshalText(b []byte) error {
} }
type RaceParam struct { type RaceParam struct {
RawSpeed int `json:"rawSpeed"` RawSpeed int `json:"rawSpeed"`
RawStamina int `json:"rawStamina"` RawStamina int `json:"rawStamina"`
RawPow int `json:"rawPow"` RawPow int `json:"rawPow"`
RawGuts int `json:"rawGuts"` RawGuts int `json:"rawGuts"`
RawWiz int `json:"rawWiz"` RawWiz int `json:"rawWiz"`
BaseSpeed float32 `json:"baseSpeed"` BaseSpeed float32 `json:"baseSpeed"`
BaseStamina float32 `json:"baseStamina"` BaseStamina float32 `json:"baseStamina"`
BasePow float32 `json:"basePow"` BasePow float32 `json:"basePow"`
BaseGuts float32 `json:"baseGuts"` BaseGuts float32 `json:"baseGuts"`
BaseWiz float32 `json:"baseWiz"` BaseWiz float32 `json:"baseWiz"`
Motivation string `json:"motivation"` Motivation Motivation `json:"motivation"`
MotivationCoef float32 `json:"motivationCoef"` 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 { type SkillArray struct {
SkillID int `json:"skill_id"` SkillID horse.SkillID `json:"skill_id"`
Level int `json:"level"` Level int32 `json:"level"`
} }
type RaceResultArray struct { type RaceResult struct {
Turn int `json:"turn"` Turn int32 `json:"turn"`
ProgramID int `json:"program_id"` ProgramID int32 `json:"program_id"`
Weather int `json:"weather"` Weather int32 `json:"weather"`
GroundCondition int `json:"ground_condition"` GroundCondition int32 `json:"ground_condition"`
RunningStyle int `json:"running_style"` RunningStyle int32 `json:"running_style"`
Popularity int `json:"popularity"` Popularity int32 `json:"popularity"`
ResultRank int `json:"result_rank"` ResultRank int32 `json:"result_rank"`
ResultTime int `json:"result_time"` ResultTime int32 `json:"result_time"`
PrizeMoney int `json:"prize_money"` PrizeMoney int32 `json:"prize_money"`
} }
type ResponseHorseData struct {
FrameOrder int `json:"frame_order"` type RaceHorseData struct {
ViewerID int64 `json:"viewer_id"` FrameOrder int32 `json:"frame_order"`
TrainerName string `json:"trainer_name"` ViewerID int64 `json:"viewer_id"`
OwnerViewerID int `json:"owner_viewer_id"` TrainerName string `json:"trainer_name"`
OwnerTrainerName string `json:"owner_trainer_name"` OwnerViewerID int64 `json:"owner_viewer_id"`
SingleModeCharaID int `json:"single_mode_chara_id"` OwnerTrainerName string `json:"owner_trainer_name"`
TrainedCharaID int `json:"trained_chara_id"` SingleModeCharaID int32 `json:"single_mode_chara_id"`
NicknameID int `json:"nickname_id"` TrainedCharaID int32 `json:"trained_chara_id"`
CharaID int `json:"chara_id"` NicknameID int32 `json:"nickname_id"`
CardID int `json:"card_id"` CharaID int32 `json:"chara_id"`
MobID int `json:"mob_id"` CardID int32 `json:"card_id"`
Rarity int `json:"rarity"` MobID int32 `json:"mob_id"`
TalentLevel int `json:"talent_level"` Rarity int32 `json:"rarity"`
SkillArray []SkillArray `json:"skill_array"` TalentLevel int32 `json:"talent_level"`
Stamina int `json:"stamina"` SkillArray []SkillArray `json:"skill_array"`
Speed int `json:"speed"` Stamina int32 `json:"stamina"`
Pow int `json:"pow"` Speed int32 `json:"speed"`
Guts int `json:"guts"` Pow int32 `json:"pow"`
Wiz int `json:"wiz"` Guts int32 `json:"guts"`
RunningStyle int `json:"running_style"` Wiz int32 `json:"wiz"`
RaceDressID int `json:"race_dress_id"` RunningStyle int32 `json:"running_style"`
CharaColorType int `json:"chara_color_type"` RaceDressID int32 `json:"race_dress_id"`
NpcType int `json:"npc_type"` CharaColorType int32 `json:"chara_color_type"`
FinalGrade int `json:"final_grade"` NpcType int32 `json:"npc_type"`
Popularity int `json:"popularity"` FinalGrade int32 `json:"final_grade"`
PopularityMarkRankArray []int `json:"popularity_mark_rank_array"` Popularity int32 `json:"popularity"`
ProperDistanceShort int `json:"proper_distance_short"` PopularityMarkRankArray []int32 `json:"popularity_mark_rank_array"`
ProperDistanceMile int `json:"proper_distance_mile"` ProperDistanceShort int32 `json:"proper_distance_short"`
ProperDistanceMiddle int `json:"proper_distance_middle"` ProperDistanceMile int32 `json:"proper_distance_mile"`
ProperDistanceLong int `json:"proper_distance_long"` ProperDistanceMiddle int32 `json:"proper_distance_middle"`
ProperRunningStyleNige int `json:"proper_running_style_nige"` ProperDistanceLong int32 `json:"proper_distance_long"`
ProperRunningStyleSenko int `json:"proper_running_style_senko"` ProperRunningStyleNige int32 `json:"proper_running_style_nige"`
ProperRunningStyleSashi int `json:"proper_running_style_sashi"` ProperRunningStyleSenko int32 `json:"proper_running_style_senko"`
ProperRunningStyleOikomi int `json:"proper_running_style_oikomi"` ProperRunningStyleSashi int32 `json:"proper_running_style_sashi"`
ProperGroundTurf int `json:"proper_ground_turf"` ProperRunningStyleOikomi int32 `json:"proper_running_style_oikomi"`
ProperGroundDirt int `json:"proper_ground_dirt"` ProperGroundTurf int32 `json:"proper_ground_turf"`
Motivation int `json:"motivation"` ProperGroundDirt int32 `json:"proper_ground_dirt"`
WinSaddleIDArray []int `json:"win_saddle_id_array"` Motivation int32 `json:"motivation"`
RaceResultArray []RaceResultArray `json:"race_result_array"` WinSaddleIDArray []int32 `json:"win_saddle_id_array"`
TeamID int `json:"team_id"` RaceResultArray []RaceResult `json:"race_result_array"`
TeamMemberID int `json:"team_member_id"` TeamID int32 `json:"team_id"`
TeamRank int `json:"team_rank"` TeamMemberID int32 `json:"team_member_id"`
SingleModeWinCount int `json:"single_mode_win_count"` TeamRank int32 `json:"team_rank"`
ItemIDArray []any `json:"item_id_array"` SingleModeWinCount int32 `json:"single_mode_win_count"`
MotivationChangeFlag int `json:"motivation_change_flag"` ItemIDArray []int32 `json:"item_id_array"`
FrameOrderChangeFlag int `json:"frame_order_change_flag"` 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"` type RunningStyleEx int8
FactorID int `json:"factorId"`
BaseFactorID int `json:"baseFactorId"` //go:generate go run golang.org/x/tools/cmd/stringer@v0.41.0 -type RunningStyleEx -linecomment
UpgradeHistoryList []any `json:"upgradeHistoryList"` 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 { type SuccessionCharaList struct {
PositionID int `json:"positionId"` PositionID int32 `json:"positionId"`
CardID int `json:"cardId"` CardID int32 `json:"cardId"`
Rarity int `json:"rarity"` Rarity int32 `json:"rarity"`
Level int `json:"level"` Level int32 `json:"level"`
Rank int `json:"rank"` Rank int32 `json:"rank"`
FactorDataArray []FactorDataArray `json:"factorDataArray"` FactorDataArray []FactorData `json:"factorDataArray"`
SortedFactorList []any `json:"sortedFactorList"` SortedFactorList []umadump.Never `json:"sortedFactorList"`
SortedFactorListForProfileCard []any `json:"sortedFactorListForProfileCard"` SortedFactorListForProfileCard []umadump.Never `json:"sortedFactorListForProfileCard"`
OwnerViewerID int `json:"ownerViewerId"` OwnerViewerID int64 `json:"ownerViewerId"`
IsPlayer bool `json:"isPlayer"` IsPlayer bool `json:"isPlayer"`
WinSaddleArray []any `json:"winSaddleArray"` WinSaddleArray []umadump.Never `json:"winSaddleArray"`
WinSaddleIDArray []int `json:"winSaddleIdArray"` 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"` type SupportCard struct {
Level int `json:"level"` Position int32 `json:"position"`
Master Master `json:"master"` SupportCardID horse.SupportCardID `json:"supportCardId"`
LimitBreakCount int32 `json:"limitBreakCount"`
Exp int32 `json:"exp"`
} }
type SupportCardArray struct {
Position int `json:"position"` type SingleModeRaceResult struct {
SupportCardID int `json:"supportCardId"`
LimitBreakCount int `json:"limitBreakCount"`
Exp int `json:"exp"`
}
type SingleModeRaceResultArray struct {
Turn int `json:"turn"` Turn int `json:"turn"`
ProgramID int `json:"programId"` ProgramID int `json:"programId"`
RaceInstanceID int `json:"raceInstanceId"` RaceInstanceID int `json:"raceInstanceId"`
@@ -664,172 +877,46 @@ type SingleModeRaceResultArray struct {
ResultRank int `json:"resultRank"` ResultRank int `json:"resultRank"`
ScenarioID int `json:"scenarioId"` ScenarioID int `json:"scenarioId"`
} }
type MasterCardData struct {
} type CardRarity int8
type MasterCharaData struct {
} //go:generate go run golang.org/x/tools/cmd/stringer@v0.41.0 -type CardRarity -linecomment -trimprefix Card
type MasterCardRarityData struct { const (
} CardNone CardRarity = iota // NONE
type TrainedCharaDataAccessor struct { CardRare1
} CardRare2
type TrainedCharaData struct { CardRare3
ID int `json:"id"` CardRare4
IsSaved bool `json:"isSaved"` CardRare5
ViewerID int64 `json:"viewerId"` )
OwnerViewerID int `json:"ownerViewerId"`
OwnerTrainedCharaID int `json:"ownerTrainedCharaId"` func (r CardRarity) AppendText(b []byte) ([]byte, error) {
UseType int `json:"useType"` return append(b, r.String()...), nil
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 RaceHorse struct { func (r CardRarity) MarshalText() ([]byte, error) {
HorseIndex int `json:"horseIndex"` return r.AppendText(nil)
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"`
} }
type RaceRewardSingle struct { func (r *CardRarity) UnmarshalText(b []byte) error {
// TODO(zeph): this 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 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, skill_id: 0,
name: 'Skill data is loading...', name: 'Skill data is loading...',
description: 'Skill data is still loading, or else something went wrong.', description: 'Skill data is still loading, or else something went wrong.',
group_skills: [],
group: 0, group: 0,
rarity: 1, rarity: 1,
group_rate: 1, group_rate: 1,
+4
View File
@@ -4,6 +4,10 @@ import type { RegionalName } from '$lib/regional-name';
* Lobby conversation data. * Lobby conversation data.
*/ */
export interface Conversation { export interface Conversation {
/**
* Gallery ID.
*/
id: number;
/** /**
* Character who owns the conversation as a gallery entry. * Character who owns the conversation as a gallery entry.
*/ */
+16
View File
@@ -14,6 +14,10 @@ export interface Skill {
* Regional skil description. * Regional skil description.
*/ */
description: string; description: string;
/**
* Other skills in the skill group.
*/
group_skills: GroupSkill[];
/** /**
* Skill group ID. * Skill group ID.
*/ */
@@ -58,6 +62,18 @@ export interface Skill {
icon_id: number; 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 { export enum DurationScale {
Direct = 1, Direct = 1,
FrontDistance = 2, FrontDistance = 2,
+3 -2
View File
@@ -771,16 +771,17 @@ export function conservePowerAccel(
distanceType: Distance, distanceType: Distance,
style: RunningStyle, style: RunningStyle,
rawPower: number, rawPower: number,
basePower: number,
powerSkillBonus?: number, powerSkillBonus?: number,
spotStruggled?: boolean, spotStruggled?: boolean,
rushed?: boolean, rushed?: boolean,
): number { ): number {
const power = rawPower + (powerSkillBonus ?? 0); const power = basePower + 0.5 * Math.max(rawPower - 1200, 0) + (powerSkillBonus ?? 0);
if (power <= 1200) { if (power <= 1200) {
return 0; return 0;
} }
const sdc = conserveStratDistCoef[style][distanceType]; 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; 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, requiredCharas,
convoCharas, convoCharas,
} from '$lib/data/convo'; } from '$lib/data/convo';
import * as talk from '$lib/umadump/talk';
import CharaPick from '$lib/CharaPick.svelte'; import CharaPick from '$lib/CharaPick.svelte';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import CharaIcon from '$lib/CharaIcon.svelte'; import CharaIcon from '$lib/CharaIcon.svelte';
import { SvelteSet } from 'svelte/reactivity';
let conversations: Conversation[] = $state([]); let allConvos: Conversation[] = $state([]);
const convoMap = $derived(byChara(conversations)); const popular = $derived(groupPopulars(allConvos));
const popular = $derived(groupPopulars(conversations));
let characters: Character[] = $state([]); let characters: Character[] = $state([]);
const selCharas = $derived(characters.filter((c) => convoMap.has(c.chara_id)));
const names = $derived(charaNames(characters)); const names = $derived(charaNames(characters));
const minSuggest = 8; const minSuggest = 8;
onMount(async () => { onMount(async () => {
[conversations, characters] = await Promise.all([conversation(), character()]); [allConvos, characters] = await Promise.all([conversation(), character()]);
}); });
let chara: Character | undefined = $state(); let chara: Character | undefined = $state();
let convo = $state(1); 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 options = $derived(convoMap.get(chara?.chara_id ?? 0) ?? []);
const cur = $derived(options.find((c) => c.number === convo)); const cur = $derived(options.find((c) => c.number === convo));
const charaIDs = $derived(cur != null ? convoCharas(cur) : []); const charaIDs = $derived(cur != null ? convoCharas(cur) : []);
@@ -47,12 +73,12 @@
</script> </script>
<h1 class="text-4xl">Lobby Conversations</h1> <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="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="m-4 flex-1 md:mt-3"> <div class="md:col-span-3">
<label for="chara" class="hidden md:inline">Character</label> <label for="chara" class="hidden md:inline">Character</label>
<CharaPick id="chara" characters={selCharas} class="w-full" bind:value={chara} required /> <CharaPick id="chara" characters={selCharas} class="w-full" bind:value={chara} required />
</div> </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> <label for="convo" class="hidden md:inline">Conversation</label>
<select id="convo" bind:value={convo} class="w-full"> <select id="convo" bind:value={convo} class="w-full">
{#each options as opt (opt.number)} {#each options as opt (opt.number)}
@@ -60,6 +86,29 @@
{/each} {/each}
</select> </select>
</div> </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> </div>
{#if cur} {#if cur}
<div class="shadow-sm transition-shadow hover:shadow-md"> <div class="shadow-sm transition-shadow hover:shadow-md">
+11 -29
View File
@@ -26,6 +26,7 @@
import Article from '../Article.svelte'; import Article from '../Article.svelte';
import Sec from '../Sec.svelte'; import Sec from '../Sec.svelte';
import * as skill from '$lib/data/skill'; import * as skill from '$lib/data/skill';
import RaceLenSlider from '$lib/RaceLenSlider.svelte';
let raceLen = $state(2000); let raceLen = $state(2000);
let secSpeedStyle = $state(RunningStyle.FrontRunner); let secSpeedStyle = $state(RunningStyle.FrontRunner);
@@ -224,10 +225,10 @@
}, },
]); ]);
const fcSeries: ComputedSeries[] = $derived([ const fcSeries: ComputedSeries[] = $derived([
{ label: 'Front Runner', y: (x) => conservePowerAccel(distance(raceLen), RunningStyle.FrontRunner, x) }, { label: 'Front Runner', y: (x, raw) => conservePowerAccel(distance(raceLen), RunningStyle.FrontRunner, raw, x) },
{ label: 'Pace Chaser', y: (x) => conservePowerAccel(distance(raceLen), RunningStyle.PaceChaser, x) }, { label: 'Pace Chaser', y: (x, raw) => conservePowerAccel(distance(raceLen), RunningStyle.PaceChaser, raw, x) },
{ label: 'Late Surger', y: (x) => conservePowerAccel(distance(raceLen), RunningStyle.LateSurger, x) }, { label: 'Late Surger', y: (x, raw) => conservePowerAccel(distance(raceLen), RunningStyle.LateSurger, raw, x) },
{ label: 'End Closer', y: (x) => conservePowerAccel(distance(raceLen), RunningStyle.EndCloser, x) }, { label: 'End Closer', y: (x, raw) => conservePowerAccel(distance(raceLen), RunningStyle.EndCloser, raw, x) },
]); ]);
</script> </script>
@@ -514,23 +515,13 @@
plotOptions={{ color: { legend: true } }} plotOptions={{ color: { legend: true } }}
/> />
<div class="mx-auto flex w-full md:max-w-2xl"> <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> <label class="my-auto mr-2 hidden flex-none pr-2 text-right text-sm md:inline" for="secSpeedRaceLen">Race Length</label>
<input <RaceLenSlider bind:value={raceLen} />
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>
</div> </div>
</div> </div>
<p> <p>
The exact mechanics of conserving power are unclear at this time. What has been confirmed is that rushed and <a Rushed and <a href="#spot-struggle">spot struggle</a> reduce both the duration and effectiveness of full charge, potentially to
href="#spot-struggle">spot struggle</a zero. In practice, this appears to be a concern primarily on sprints, which already have a -55% penalty to full charge duration.
> both reduce the chance for it to fire.
</p> </p>
<Sec h={3} id="lane-combo">Lane Combo</Sec> <Sec h={3} id="lane-combo">Lane Combo</Sec>
@@ -670,17 +661,8 @@
plotOptions={{ color: { legend: true } }} plotOptions={{ color: { legend: true } }}
/> />
<div class="mx-auto flex w-full md:max-w-2xl"> <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> <label class="my-auto mr-2 hidden flex-1 pr-2 text-right text-sm md:inline" for="secSpeedRaceLen">Race Length</label>
<input <RaceLenSlider bind:value={raceLen} />
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 hidden flex-1 pr-2 text-right text-sm md:inline" for="secSpeedStyle">Style</label> <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}> <select class="my-auto flex-1 text-sm" id="secSpeedStyle" bind:value={secSpeedStyle}>
{#each RUNNING_STYLES as [name, style] (style)} {#each RUNNING_STYLES as [name, style] (style)}
+16 -17
View File
@@ -5,6 +5,7 @@
import * as race from '$lib/race'; import * as race from '$lib/race';
import StatChart from '$lib/StatChart.svelte'; import StatChart from '$lib/StatChart.svelte';
import { resolve } from '$app/paths'; import { resolve } from '$app/paths';
import RaceLenSlider from '$lib/RaceLenSlider.svelte';
function modemean(x: number, bonus: number, p: number): number { function modemean(x: number, bonus: number, p: number): number {
return x * (1 - p) + x * bonus * p; 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 laneChange: ComputedSeries[] = [{ label: 'Lane Change Target Speed', y: (x) => race.laneChangeSpeed(x) }];
const conservePower: Array<ComputedSeries | null> = $derived([ const conservePower: Array<ComputedSeries | null> = $derived([
{ label: 'Conserved Power', y: (_raw, x) => race.conservePowerAccel(distanceType, style, x, 0, false, false) }, { label: 'Conserved Power', y: (x, raw) => race.conservePowerAccel(distanceType, style, raw, x, 0, false, false) },
{ label: 'Rushed', y: (_raw, x) => race.conservePowerAccel(distanceType, style, x, 0, false, true) }, { label: 'Rushed', y: (x, raw) => race.conservePowerAccel(distanceType, style, raw, x, 0, false, true) },
styleIsFront 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, : null,
]); ]);
const gutsSpurt: Array<ComputedSeries | null> = $derived([ 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"> <div class="mx-auto grid w-full grid-cols-3 gap-y-6 md:max-w-2xl md:grid-cols-7">
{#if pick?.len} {#if pick?.len}
<input <div class="col-span-3 my-auto mr-4">
class="col-span-2 my-auto w-full justify-self-center-safe" <RaceLenSlider bind:value={raceLen} />
type="range" </div>
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>
{/if} {/if}
{#if pick?.style} {#if pick?.style}
<select class="col-span-1 my-auto text-sm md:col-span-2" id="stylePick" bind:value={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 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. modifiers for ground conditions and career mode do not apply.
</p> </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 })} {@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> <Sec h={2} id="guts">Guts</Sec>
+4 -10
View File
@@ -4,6 +4,7 @@
import * as accel from '$lib/racelib/accel'; import * as accel from '$lib/racelib/accel';
import type { Attachment } from 'svelte/attachments'; import type { Attachment } from 'svelte/attachments';
import CalcInfo from '../CalcInfo.svelte'; import CalcInfo from '../CalcInfo.svelte';
import RaceLenSlider from '$lib/RaceLenSlider.svelte';
let rawSpeed = $state(1200); let rawSpeed = $state(1200);
let rawPower = $state(1200); let rawPower = $state(1200);
@@ -231,16 +232,9 @@
</div> </div>
<div class="mb-auto grid max-w-4xl grid-cols-6 rounded-md text-center shadow-md ring md:col-span-6"> <div class="mb-auto grid max-w-4xl grid-cols-6 rounded-md text-center shadow-md ring md:col-span-6">
<input <div class="col-span-full m-4">
class="col-span-4 m-4 w-full justify-self-center-safe" <RaceLenSlider bind:value={raceLen} />
type="range" </div>
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>
<select id="surface" bind:value={surface} class="col-span-2 mx-2 mb-4"> <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.Turf}>Turf</option>
<option value={race.Surface.Dirt}>Dirt</option> <option value={race.Surface.Dirt}>Dirt</option>
+4 -10
View File
@@ -4,6 +4,7 @@
import * as accel from '$lib/racelib/accel'; import * as accel from '$lib/racelib/accel';
import type { Attachment } from 'svelte/attachments'; import type { Attachment } from 'svelte/attachments';
import CalcInfo from '../CalcInfo.svelte'; import CalcInfo from '../CalcInfo.svelte';
import RaceLenSlider from '$lib/RaceLenSlider.svelte';
let rawPower = $state(1200); let rawPower = $state(1200);
let rawGuts = $state(1200); let rawGuts = $state(1200);
@@ -216,16 +217,9 @@
</div> </div>
</div> </div>
<div class="mb-auto grid max-w-4xl grid-cols-6 rounded-md text-center shadow-md ring md:col-span-3"> <div class="mb-auto grid max-w-4xl grid-cols-6 rounded-md text-center shadow-md ring md:col-span-3">
<input <div class="col-span-6 mx-4 mt-4">
class="col-span-4 m-4 w-full justify-self-center-safe" <RaceLenSlider bind:value={raceLen} />
type="range" </div>
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>
<select id="surface" bind:value={surface} class="col-span-2 m-4"> <select id="surface" bind:value={surface} class="col-span-2 m-4">
<option value={race.Surface.Turf}>Turf</option> <option value={race.Surface.Turf}>Turf</option>
<option value={race.Surface.Dirt}>Dirt</option> <option value={race.Surface.Dirt}>Dirt</option>
+2 -2
View File
@@ -18,6 +18,7 @@
import StatChart from '$lib/StatChart.svelte'; import StatChart from '$lib/StatChart.svelte';
import { resolve } from '$app/paths'; import { resolve } from '$app/paths';
import SpeedDur from './SpeedDur.svelte'; import SpeedDur from './SpeedDur.svelte';
import RaceLenSlider from '$lib/RaceLenSlider.svelte';
let rawPower = $state(1200); let rawPower = $state(1200);
let rawGuts = $state(1200); let rawGuts = $state(1200);
@@ -112,8 +113,7 @@
</select> </select>
</div> </div>
<div class="m-4 md:col-span-2 md:col-start-2"> <div class="m-4 md:col-span-2 md:col-start-2">
<label for="raceLen">Race Distance</label> <RaceLenSlider bind:value={raceLen} />
<input type="number" id="raceLen" required min="1000" max="3600" step="100" bind:value={raceLen} class="w-full" />
</div> </div>
<div class="m-4 self-center md:col-span-2"> <div class="m-4 self-center md:col-span-2">
<label for="isRunaway" class="mr-1 align-middle">Runaway</label> <label for="isRunaway" class="mr-1 align-middle">Runaway</label>
+2 -2
View File
@@ -1,6 +1,7 @@
<script lang="ts"> <script lang="ts">
import type { ComputedSeries } from '$lib/chart'; import type { ComputedSeries } from '$lib/chart';
import * as race from '$lib/race'; import * as race from '$lib/race';
import RaceLenSlider from '$lib/RaceLenSlider.svelte';
import StatChart from '$lib/StatChart.svelte'; import StatChart from '$lib/StatChart.svelte';
import CalcInfo from '../CalcInfo.svelte'; import CalcInfo from '../CalcInfo.svelte';
@@ -98,8 +99,7 @@
</select> </select>
</div> </div>
<div class="m-4 md:col-start-2"> <div class="m-4 md:col-start-2">
<label for="raceLen">Race Distance</label> <RaceLenSlider bind:value={raceLen} />
<input type="number" id="raceLen" required min="1000" max="3600" step="100" bind:value={raceLen} class="w-full" />
</div> </div>
<div class="m-4 self-center"> <div class="m-4 self-center">
<label for="isCareer" class="mr-1 align-middle">In Career</label> <label for="isCareer" class="mr-1 align-middle">In Career</label>
+2 -10
View File
@@ -1,6 +1,7 @@
<script lang="ts"> <script lang="ts">
import * as course from '$lib/course'; import * as course from '$lib/course';
import * as race from '$lib/race'; import * as race from '$lib/race';
import RaceLenSlider from '$lib/RaceLenSlider.svelte';
import * as recovery from '$lib/racelib/recovery'; import * as recovery from '$lib/racelib/recovery';
import HPChart from './HPChart.svelte'; import HPChart from './HPChart.svelte';
import InfoPanels from './InfoPanels.svelte'; import InfoPanels from './InfoPanels.svelte';
@@ -277,16 +278,7 @@
</div> </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="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"> <div class="m-4 flex md:col-span-2">
<input <RaceLenSlider bind:value={raceLen} />
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>
</div> </div>
<select id="surface" class="m-4" bind:value={surface}> <select id="surface" class="m-4" bind:value={surface}>
<option value={race.Surface.Turf}>Turf</option> <option value={race.Surface.Turf}>Turf</option>