564 lines
14 KiB
Go
564 lines
14 KiB
Go
package alt_advancement
|
|
|
|
import (
|
|
"fmt"
|
|
"time"
|
|
)
|
|
|
|
// LoadAltAdvancements loads all AA definitions from the database
|
|
func (db *DatabaseImpl) LoadAltAdvancements() error {
|
|
query := `
|
|
SELECT nodeid, minlevel, spellcrc, name, description, aa_list_fk,
|
|
icon_id, icon_backdrop, xcoord, ycoord, pointspertier, maxtier,
|
|
firstparentid, firstparentrequiredtier, displayedclassification,
|
|
requiredclassification, classificationpointsrequired,
|
|
pointsspentintreetounlock, title, titlelevel
|
|
FROM spell_aa_nodelist
|
|
ORDER BY aa_list_fk, ycoord, xcoord`
|
|
|
|
rows, err := db.db.Query(query)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to query AA data: %v", err)
|
|
}
|
|
defer rows.Close()
|
|
|
|
loadedCount := 0
|
|
for rows.Next() {
|
|
data := &AltAdvanceData{
|
|
CreatedAt: time.Now(),
|
|
UpdatedAt: time.Now(),
|
|
}
|
|
|
|
err := rows.Scan(
|
|
&data.NodeID,
|
|
&data.MinLevel,
|
|
&data.SpellCRC,
|
|
&data.Name,
|
|
&data.Description,
|
|
&data.Group,
|
|
&data.Icon,
|
|
&data.Icon2,
|
|
&data.Col,
|
|
&data.Row,
|
|
&data.RankCost,
|
|
&data.MaxRank,
|
|
&data.RankPrereqID,
|
|
&data.RankPrereq,
|
|
&data.ClassReq,
|
|
&data.Tier,
|
|
&data.ReqPoints,
|
|
&data.ReqTreePoints,
|
|
&data.LineTitle,
|
|
&data.TitleLevel,
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to scan AA data: %v", err)
|
|
}
|
|
|
|
// Set spell ID to node ID if not provided separately
|
|
data.SpellID = data.NodeID
|
|
|
|
// Validate and add to master list
|
|
if err := db.masterAAList.AddAltAdvancement(data); err != nil {
|
|
// Log warning but continue loading
|
|
if db.logger != nil {
|
|
db.logger.Printf("Warning: failed to add AA node %d: %v", data.NodeID, err)
|
|
}
|
|
continue
|
|
}
|
|
|
|
loadedCount++
|
|
}
|
|
|
|
if err = rows.Err(); err != nil {
|
|
return fmt.Errorf("error iterating AA rows: %v", err)
|
|
}
|
|
|
|
// Sort AAs within each group for proper display order
|
|
db.masterAAList.SortAAsByGroup()
|
|
|
|
if db.logger != nil {
|
|
db.logger.Printf("Loaded %d Alternate Advancement(s)", loadedCount)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// LoadTreeNodes loads tree node configurations from the database
|
|
func (db *DatabaseImpl) LoadTreeNodes() error {
|
|
query := `
|
|
SELECT class_id, tree_node, aa_tree_id
|
|
FROM spell_aa_class_list
|
|
ORDER BY class_id, tree_node`
|
|
|
|
rows, err := db.db.Query(query)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to query tree node data: %v", err)
|
|
}
|
|
defer rows.Close()
|
|
|
|
loadedCount := 0
|
|
for rows.Next() {
|
|
data := &TreeNodeData{}
|
|
|
|
err := rows.Scan(
|
|
&data.ClassID,
|
|
&data.TreeID,
|
|
&data.AATreeID,
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to scan tree node data: %v", err)
|
|
}
|
|
|
|
// Add to master node list
|
|
if err := db.masterNodeList.AddTreeNode(data); err != nil {
|
|
// Log warning but continue loading
|
|
if db.logger != nil {
|
|
db.logger.Printf("Warning: failed to add tree node %d: %v", data.TreeID, err)
|
|
}
|
|
continue
|
|
}
|
|
|
|
loadedCount++
|
|
}
|
|
|
|
if err = rows.Err(); err != nil {
|
|
return fmt.Errorf("error iterating tree node rows: %v", err)
|
|
}
|
|
|
|
if db.logger != nil {
|
|
db.logger.Printf("Loaded %d AA Tree Nodes", loadedCount)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// LoadPlayerAA loads AA data for a specific player
|
|
func (db *DatabaseImpl) LoadPlayerAA(characterID int32) (*AAPlayerState, error) {
|
|
playerState := NewAAPlayerState(characterID)
|
|
|
|
// Load player's AA entries
|
|
query := `
|
|
SELECT template_id, tab_id, aa_id, order, treeid
|
|
FROM character_aa
|
|
WHERE char_id = ?
|
|
ORDER BY template_id, tab_id, order`
|
|
|
|
rows, err := db.db.Query(query, characterID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to query player AA data: %v", err)
|
|
}
|
|
defer rows.Close()
|
|
|
|
// Group entries by template
|
|
templateEntries := make(map[int8][]*AAEntry)
|
|
|
|
for rows.Next() {
|
|
entry := &AAEntry{}
|
|
|
|
err := rows.Scan(
|
|
&entry.TemplateID,
|
|
&entry.TabID,
|
|
&entry.AAID,
|
|
&entry.Order,
|
|
&entry.TreeID,
|
|
)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to scan player AA entry: %v", err)
|
|
}
|
|
|
|
if templateEntries[entry.TemplateID] == nil {
|
|
templateEntries[entry.TemplateID] = make([]*AAEntry, 0)
|
|
}
|
|
templateEntries[entry.TemplateID] = append(templateEntries[entry.TemplateID], entry)
|
|
}
|
|
|
|
if err = rows.Err(); err != nil {
|
|
return nil, fmt.Errorf("error iterating player AA rows: %v", err)
|
|
}
|
|
|
|
// Create templates from loaded entries
|
|
for templateID, entries := range templateEntries {
|
|
template := NewAATemplate(templateID, GetTemplateName(templateID))
|
|
template.Entries = entries
|
|
playerState.Templates[templateID] = template
|
|
}
|
|
|
|
// Load player's AA progression data
|
|
err = db.loadPlayerAAProgress(characterID, playerState)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to load player AA progress: %v", err)
|
|
}
|
|
|
|
// Load player's AA point totals
|
|
err = db.loadPlayerAAPoints(characterID, playerState)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to load player AA points: %v", err)
|
|
}
|
|
|
|
// Initialize tabs based on loaded data
|
|
db.initializePlayerTabs(playerState)
|
|
|
|
return playerState, nil
|
|
}
|
|
|
|
// loadPlayerAAProgress loads detailed AA progression for a player
|
|
func (db *DatabaseImpl) loadPlayerAAProgress(characterID int32, playerState *AAPlayerState) error {
|
|
query := `
|
|
SELECT node_id, current_rank, points_spent, template_id, tab_id,
|
|
purchased_at, updated_at
|
|
FROM character_aa_progress
|
|
WHERE character_id = ?`
|
|
|
|
rows, err := db.db.Query(query, characterID)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to query player AA progress: %v", err)
|
|
}
|
|
defer rows.Close()
|
|
|
|
for rows.Next() {
|
|
progress := &PlayerAAData{
|
|
CharacterID: characterID,
|
|
}
|
|
|
|
var purchasedAt, updatedAt string
|
|
err := rows.Scan(
|
|
&progress.NodeID,
|
|
&progress.CurrentRank,
|
|
&progress.PointsSpent,
|
|
&progress.TemplateID,
|
|
&progress.TabID,
|
|
&purchasedAt,
|
|
&updatedAt,
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to scan player AA progress: %v", err)
|
|
}
|
|
|
|
// Parse timestamps
|
|
if progress.PurchasedAt, err = time.Parse("2006-01-02 15:04:05", purchasedAt); err != nil {
|
|
progress.PurchasedAt = time.Now()
|
|
}
|
|
if progress.UpdatedAt, err = time.Parse("2006-01-02 15:04:05", updatedAt); err != nil {
|
|
progress.UpdatedAt = time.Now()
|
|
}
|
|
|
|
playerState.AAProgress[progress.NodeID] = progress
|
|
}
|
|
|
|
return rows.Err()
|
|
}
|
|
|
|
// loadPlayerAAPoints loads AA point totals for a player
|
|
func (db *DatabaseImpl) loadPlayerAAPoints(characterID int32, playerState *AAPlayerState) error {
|
|
query := `
|
|
SELECT total_points, spent_points, available_points, banked_points,
|
|
active_template
|
|
FROM character_aa_points
|
|
WHERE character_id = ?`
|
|
|
|
row := db.db.QueryRow(query, characterID)
|
|
|
|
err := row.Scan(
|
|
&playerState.TotalPoints,
|
|
&playerState.SpentPoints,
|
|
&playerState.AvailablePoints,
|
|
&playerState.BankedPoints,
|
|
&playerState.ActiveTemplate,
|
|
)
|
|
|
|
if err != nil {
|
|
// If no record exists, initialize with defaults
|
|
if err.Error() == "sql: no rows in result set" {
|
|
playerState.TotalPoints = 0
|
|
playerState.SpentPoints = 0
|
|
playerState.AvailablePoints = 0
|
|
playerState.BankedPoints = 0
|
|
playerState.ActiveTemplate = AA_TEMPLATE_CURRENT
|
|
return nil
|
|
}
|
|
return fmt.Errorf("failed to load player AA points: %v", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// initializePlayerTabs initializes tab states based on loaded data
|
|
func (db *DatabaseImpl) initializePlayerTabs(playerState *AAPlayerState) {
|
|
// Initialize all standard tabs
|
|
for i := int8(0); i < 10; i++ {
|
|
tab := NewAATab(i, i, GetTabName(i))
|
|
tab.MaxAA = GetMaxAAForTab(i)
|
|
|
|
// Calculate points spent in this tab
|
|
pointsSpent := int32(0)
|
|
for _, progress := range playerState.AAProgress {
|
|
if progress.TabID == i {
|
|
pointsSpent += progress.PointsSpent
|
|
}
|
|
}
|
|
tab.PointsSpent = pointsSpent
|
|
tab.PointsAvailable = playerState.AvailablePoints
|
|
|
|
playerState.Tabs[i] = tab
|
|
}
|
|
}
|
|
|
|
// SavePlayerAA saves a player's AA data to the database
|
|
func (db *DatabaseImpl) SavePlayerAA(playerState *AAPlayerState) error {
|
|
if playerState == nil {
|
|
return fmt.Errorf("player state cannot be nil")
|
|
}
|
|
|
|
// Start transaction
|
|
tx, err := db.db.Begin()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to begin transaction: %v", err)
|
|
}
|
|
defer tx.Rollback()
|
|
|
|
// Save AA point totals
|
|
err = db.savePlayerAAPoints(tx, playerState)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to save player AA points: %v", err)
|
|
}
|
|
|
|
// Save AA progress
|
|
err = db.savePlayerAAProgress(tx, playerState)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to save player AA progress: %v", err)
|
|
}
|
|
|
|
// Save template entries
|
|
err = db.savePlayerAATemplates(tx, playerState)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to save player AA templates: %v", err)
|
|
}
|
|
|
|
// Commit transaction
|
|
if err = tx.Commit(); err != nil {
|
|
return fmt.Errorf("failed to commit transaction: %v", err)
|
|
}
|
|
|
|
// Update last save time
|
|
playerState.lastUpdate = time.Now()
|
|
playerState.needsSync = false
|
|
|
|
return nil
|
|
}
|
|
|
|
// savePlayerAAPoints saves AA point totals to the database
|
|
func (db *DatabaseImpl) savePlayerAAPoints(tx Transaction, playerState *AAPlayerState) error {
|
|
query := `
|
|
INSERT OR REPLACE INTO character_aa_points
|
|
(character_id, total_points, spent_points, available_points,
|
|
banked_points, active_template, updated_at)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?)`
|
|
|
|
_, err := tx.Exec(query,
|
|
playerState.CharacterID,
|
|
playerState.TotalPoints,
|
|
playerState.SpentPoints,
|
|
playerState.AvailablePoints,
|
|
playerState.BankedPoints,
|
|
playerState.ActiveTemplate,
|
|
time.Now().Format("2006-01-02 15:04:05"),
|
|
)
|
|
|
|
return err
|
|
}
|
|
|
|
// savePlayerAAProgress saves AA progression data to the database
|
|
func (db *DatabaseImpl) savePlayerAAProgress(tx Transaction, playerState *AAPlayerState) error {
|
|
// Delete existing progress
|
|
_, err := tx.Exec("DELETE FROM character_aa_progress WHERE character_id = ?", playerState.CharacterID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Insert current progress
|
|
query := `
|
|
INSERT INTO character_aa_progress
|
|
(character_id, node_id, current_rank, points_spent, template_id,
|
|
tab_id, purchased_at, updated_at)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
|
|
|
|
for _, progress := range playerState.AAProgress {
|
|
_, err = tx.Exec(query,
|
|
progress.CharacterID,
|
|
progress.NodeID,
|
|
progress.CurrentRank,
|
|
progress.PointsSpent,
|
|
progress.TemplateID,
|
|
progress.TabID,
|
|
progress.PurchasedAt.Format("2006-01-02 15:04:05"),
|
|
progress.UpdatedAt.Format("2006-01-02 15:04:05"),
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// savePlayerAATemplates saves AA template entries to the database
|
|
func (db *DatabaseImpl) savePlayerAATemplates(tx Transaction, playerState *AAPlayerState) error {
|
|
// Delete existing entries for server templates (4-6)
|
|
_, err := tx.Exec("DELETE FROM character_aa WHERE char_id = ? AND template_id BETWEEN 4 AND 6", playerState.CharacterID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Insert current template entries for server templates only
|
|
query := `
|
|
INSERT INTO character_aa
|
|
(char_id, template_id, tab_id, aa_id, order, treeid)
|
|
VALUES (?, ?, ?, ?, ?, ?)`
|
|
|
|
for _, template := range playerState.Templates {
|
|
// Only save server templates (4-6) as personal templates (1-3) are class defaults
|
|
if template.TemplateID >= 4 && template.TemplateID <= 6 {
|
|
for _, entry := range template.Entries {
|
|
_, err = tx.Exec(query,
|
|
playerState.CharacterID,
|
|
entry.TemplateID,
|
|
entry.TabID,
|
|
entry.AAID,
|
|
entry.Order,
|
|
entry.TreeID,
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// LoadPlayerAADefaults loads default AA templates for a class
|
|
func (db *DatabaseImpl) LoadPlayerAADefaults(classID int8) (map[int8][]*AAEntry, error) {
|
|
query := `
|
|
SELECT template_id, tab_id, aa_id, order, treeid
|
|
FROM character_aa_defaults
|
|
WHERE class = ?
|
|
ORDER BY template_id, tab_id, order`
|
|
|
|
rows, err := db.db.Query(query, classID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to query AA defaults: %v", err)
|
|
}
|
|
defer rows.Close()
|
|
|
|
templates := make(map[int8][]*AAEntry)
|
|
|
|
for rows.Next() {
|
|
entry := &AAEntry{}
|
|
|
|
err := rows.Scan(
|
|
&entry.TemplateID,
|
|
&entry.TabID,
|
|
&entry.AAID,
|
|
&entry.Order,
|
|
&entry.TreeID,
|
|
)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to scan AA default entry: %v", err)
|
|
}
|
|
|
|
if templates[entry.TemplateID] == nil {
|
|
templates[entry.TemplateID] = make([]*AAEntry, 0)
|
|
}
|
|
templates[entry.TemplateID] = append(templates[entry.TemplateID], entry)
|
|
}
|
|
|
|
if err = rows.Err(); err != nil {
|
|
return nil, fmt.Errorf("error iterating AA default rows: %v", err)
|
|
}
|
|
|
|
return templates, nil
|
|
}
|
|
|
|
// DeletePlayerAA removes all AA data for a player
|
|
func (db *DatabaseImpl) DeletePlayerAA(characterID int32) error {
|
|
// Start transaction
|
|
tx, err := db.db.Begin()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to begin transaction: %v", err)
|
|
}
|
|
defer tx.Rollback()
|
|
|
|
// Delete from all related tables
|
|
tables := []string{
|
|
"character_aa_points",
|
|
"character_aa_progress",
|
|
"character_aa",
|
|
}
|
|
|
|
for _, table := range tables {
|
|
query := fmt.Sprintf("DELETE FROM %s WHERE char_id = ? OR character_id = ?", table)
|
|
_, err = tx.Exec(query, characterID, characterID)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to delete from %s: %v", table, err)
|
|
}
|
|
}
|
|
|
|
// Commit transaction
|
|
if err = tx.Commit(); err != nil {
|
|
return fmt.Errorf("failed to commit transaction: %v", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetAAStatistics returns statistics about AA usage
|
|
func (db *DatabaseImpl) GetAAStatistics() (map[string]interface{}, error) {
|
|
stats := make(map[string]interface{})
|
|
|
|
// Get total players with AA data
|
|
var totalPlayers int64
|
|
err := db.db.QueryRow("SELECT COUNT(DISTINCT character_id) FROM character_aa_points").Scan(&totalPlayers)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get total players: %v", err)
|
|
}
|
|
stats["total_players_with_aa"] = totalPlayers
|
|
|
|
// Get average points spent
|
|
var avgPointsSpent float64
|
|
err = db.db.QueryRow("SELECT AVG(spent_points) FROM character_aa_points").Scan(&avgPointsSpent)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get average points spent: %v", err)
|
|
}
|
|
stats["average_points_spent"] = avgPointsSpent
|
|
|
|
// Get most popular AAs
|
|
query := `
|
|
SELECT node_id, COUNT(*) as usage_count
|
|
FROM character_aa_progress
|
|
WHERE current_rank > 0
|
|
GROUP BY node_id
|
|
ORDER BY usage_count DESC
|
|
LIMIT 10`
|
|
|
|
rows, err := db.db.Query(query)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to query popular AAs: %v", err)
|
|
}
|
|
defer rows.Close()
|
|
|
|
popularAAs := make(map[int32]int64)
|
|
for rows.Next() {
|
|
var nodeID int32
|
|
var count int64
|
|
err := rows.Scan(&nodeID, &count)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to scan popular AA: %v", err)
|
|
}
|
|
popularAAs[nodeID] = count
|
|
}
|
|
stats["popular_aas"] = popularAAs
|
|
|
|
return stats, nil
|
|
} |