Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion build.sh
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
tag=v0.1.3
tag=v0.1.4

docker build -t ghcr.io/beaverhouse/ba-data-process:$tag .
docker push ghcr.io/beaverhouse/ba-data-process:$tag
4 changes: 2 additions & 2 deletions cmd/total_analysis/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,8 @@ func main() {
// Upload result
err = logic_upload.MarshalAndUpload(
result,
"batorment/v3/total-analysis",
"analysis.json",
"batorment/v3",
"total-analysis.json",
*dryRun,
"Total analysis completed",
)
Expand Down
7 changes: 6 additions & 1 deletion cmd/update_from_schaledb/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,5 +31,10 @@ func main() {
log.Fatalf("Failed to parse SchaleDB presents: %v", err)
}

log.Println("Successfully parsed SchaleDB students")
err = schaledb.SaveI18nData(queries)
if err != nil {
log.Fatalf("Failed to save i18n data: %v", err)
}

log.Println("Successfully updated from SchaleDB")
}
4 changes: 2 additions & 2 deletions env.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ env:
- secretKey: "POSTGRES_PASSWORD"
remoteRef: "SUPABASE_POSTGRES_PASSWORD"
# Service key for personal file manager API.
- secretKey: "FILE_MANAGER_SERVICE_API_KEY"
remoteRef: "FILE_MANAGER_SERVICE_API_KEY"
- secretKey: "BA_ANALYZER_SERVICE_TOKEN"
remoteRef: "BA_ANALYZER_SERVICE_TOKEN"
# Base URL for DuckDB download.
- secretKey: "BATORMENT_DUCKDB_REMOTE_URL"
remoteRef: "BATORMENT_DUCKDB_REMOTE_URL"
95 changes: 95 additions & 0 deletions internal/db/postgres/i18n.sql.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 10 additions & 0 deletions internal/db/postgres/models.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions internal/db/postgres/querier.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 14 additions & 0 deletions internal/db/postgres/sql/query/i18n.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
-- name: UpsertI18n :exec
INSERT INTO batorment_v3.i18n (category, key, name_ko, name_ja, name_en, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, NOW(), NOW())
ON CONFLICT (category, key) DO UPDATE SET
name_ko = EXCLUDED.name_ko,
name_ja = EXCLUDED.name_ja,
name_en = EXCLUDED.name_en,
updated_at = NOW();

-- name: GetI18nByCategory :many
SELECT * FROM batorment_v3.i18n WHERE category = $1;

-- name: GetI18n :one
SELECT * FROM batorment_v3.i18n WHERE category = $1 AND key = $2;
13 changes: 13 additions & 0 deletions internal/db/postgres/sql/schema/schema_251007.sql
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,23 @@ CREATE TABLE batorment_v3.presents (
updated_at TIMESTAMP
);

-- Table for i18n translations (school, club, etc.)
CREATE TABLE batorment_v3.i18n (
category VARCHAR(20) NOT NULL,
key VARCHAR(50) NOT NULL,
name_ko VARCHAR(100) NOT NULL,
name_ja VARCHAR(100) NOT NULL,
name_en VARCHAR(100) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP,
PRIMARY KEY (category, key)
);

-- Indexes for better performance
CREATE INDEX IF NOT EXISTS idx_students_name ON batorment_v3.students(name_ko);
CREATE INDEX IF NOT EXISTS idx_students_name_ja ON batorment_v3.students(name_ja);
CREATE INDEX IF NOT EXISTS idx_students_details ON batorment_v3.students USING GIN (detail);
CREATE INDEX IF NOT EXISTS idx_presents_name ON batorment_v3.presents(name_ko);
CREATE INDEX IF NOT EXISTS idx_presents_tags ON batorment_v3.presents USING GIN (tags);
CREATE INDEX IF NOT EXISTS idx_i18n_category ON batorment_v3.i18n(category);

48 changes: 48 additions & 0 deletions internal/logic/analysis/analysis.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"io"
"log"
"net/http"
"sort"
"time"

"ba-torment-data-process/internal/types"
Expand Down Expand Up @@ -83,9 +84,56 @@ func RunTotalAnalysis(partyDataMap map[string]*types.BATormentPartyData, sortedC
characterAnalyses := RunCharacterAnalyses(partyDataMap, sortedContentIDs)
log.Printf("Completed character analyses: %d characters", len(characterAnalyses))

// Calculate and assign overall rankings
log.Println("Calculating overall rankings...")
AssignOverallRankings(characterAnalyses)
log.Println("Completed overall ranking calculation")

return &types.TotalAnalysisOutput{
GeneratedAt: time.Now().Format(time.RFC3339),
RaidAnalyses: raidAnalyses,
CharacterAnalyses: characterAnalyses,
}
}

// AssignOverallRankings calculates and assigns overall usage rankings to characterAnalyses
func AssignOverallRankings(characterAnalyses []types.CharacterAnalysisResult) {
// Calculate total usage for each character
for i := range characterAnalyses {
totalUsage := 0
for _, ru := range characterAnalyses[i].UsageHistory {
totalUsage += ru.UserCount
}
characterAnalyses[i].TotalUsage = totalUsage
}

// Create index slice for sorting
indices := make([]int, len(characterAnalyses))
for i := range indices {
indices[i] = i
}

// Sort indices by total usage (descending)
sort.Slice(indices, func(i, j int) bool {
return characterAnalyses[indices[i]].TotalUsage > characterAnalyses[indices[j]].TotalUsage
})

// Assign overall ranks based on sorted order
for rank, idx := range indices {
characterAnalyses[idx].OverallRank = rank + 1
}

// Calculate category ranks (striker: 1xxxx, special: 2xxxx)
strikerRank := 1
specialRank := 1
for _, idx := range indices {
studentID := characterAnalyses[idx].StudentID
if studentID >= 10000 && studentID < 20000 {
characterAnalyses[idx].CategoryRank = strikerRank
strikerRank++
} else if studentID >= 20000 && studentID < 30000 {
characterAnalyses[idx].CategoryRank = specialRank
specialRank++
}
}
}
70 changes: 49 additions & 21 deletions internal/logic/analysis/character_analysis.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,11 @@ func RunCharacterAnalyses(partyDataMap map[string]*types.BATormentPartyData, sor
// sortedContentIDs provides the order for usageHistory (sorted by start_date)
func AnalyzeCharacter(studentID int, partyDataMap map[string]*types.BATormentPartyData, sortedContentIDs []string) types.CharacterAnalysisResult {
var usageHistory []types.RaidUsage
var starDistribution []types.RaidStarDistribution

// Group star distribution by groupID (e.g., "3S26-1", "3S26-3" -> "3S26")
groupStar := make(map[string]map[string]int)
groupAsOwn := make(map[string]int)
var groupOrder []string // Track order of first appearance

totalAsAssist := 0
totalAsOwn := 0
Expand All @@ -47,19 +51,24 @@ func AnalyzeCharacter(studentID int, partyDataMap map[string]*types.BATormentPar
}
raidUsage, raidStar, asAssist, asOwn, coChars, appearances := analyzeCharacterInRaid(studentID, partyData)

// usageHistory: individual raid entries (no grouping)
usageHistory = append(usageHistory, types.RaidUsage{
RaidID: raidID,
UserCount: raidUsage.UserCount,
LunaticUserCount: raidUsage.LunaticUserCount,
})

// Only include star distribution if own usage (excluding assist) >= 200 (1%)
if asOwn >= 200 {
starDistribution = append(starDistribution, types.RaidStarDistribution{
RaidID: raidID,
Distribution: raidStar,
})
// starDistribution: group by groupID
groupID := ExtractGroupID(raidID)
if _, exists := groupStar[groupID]; !exists {
groupStar[groupID] = make(map[string]int)
groupOrder = append(groupOrder, groupID)
}

for key, count := range raidStar {
groupStar[groupID][key] += count
}
groupAsOwn[groupID] += asOwn

totalAsAssist += asAssist
totalAsOwn += asOwn
Expand All @@ -70,6 +79,19 @@ func AnalyzeCharacter(studentID int, partyDataMap map[string]*types.BATormentPar
totalAppearances += appearances
}

// Get latest star distribution (200+ own usage)
var starDistribution *types.RaidStarDistribution
for i := len(groupOrder) - 1; i >= 0; i-- {
groupID := groupOrder[i]
if groupAsOwn[groupID] >= 200 {
starDistribution = &types.RaidStarDistribution{
RaidID: groupID,
Distribution: groupStar[groupID],
}
break
}
}

// Calculate synergy (top 3, >= 5%)
topSynergyChars := calculateTopSynergy(coUsageCount, totalAppearances, 3)

Expand Down Expand Up @@ -112,22 +134,25 @@ func analyzeCharacterInRaid(studentID int, partyData *types.BATormentPartyData)
}

isLunatic := party.Score >= constants.LunaticMinScore
foundInParty := false
var partyMembers []int
foundInAnySquad := false

for _, members := range party.PartyData {
for _, member := range members {
for _, squad := range party.PartyData {
var squadMembers []int
foundInThisSquad := false

for _, member := range squad {
if member == 0 {
continue
}

memberStudentID, star, weaponStar, isAssist := ParseStudentDetailID(member)

// Collect party members for synergy
partyMembers = append(partyMembers, memberStudentID)
// Collect squad members for synergy
squadMembers = append(squadMembers, memberStudentID)

if memberStudentID == studentID {
foundInParty = true
foundInThisSquad = true
foundInAnySquad = true

if isAssist {
asAssist++
Expand All @@ -145,17 +170,20 @@ func analyzeCharacterInRaid(studentID int, partyData *types.BATormentPartyData)
}
}
}
}

if foundInParty {
appearances++
// Count co-usage characters
for _, memberID := range partyMembers {
if memberID != studentID {
coChars[memberID]++
// Count co-usage only for squads where this character appears
if foundInThisSquad {
for _, memberID := range squadMembers {
if memberID != studentID {
coChars[memberID]++
}
}
}
}

if foundInAnySquad {
appearances++
}
}

usage = types.RaidUsage{
Expand Down
Loading