package alt_advancement import ( "database/sql" "fmt" "time" "eq2emu/internal/database" ) // AltAdvancement represents an Alternate Advancement node with database operations type AltAdvancement struct { // Core identification SpellID int32 `json:"spell_id" db:"spell_id"` NodeID int32 `json:"node_id" db:"node_id"` SpellCRC int32 `json:"spell_crc" db:"spell_crc"` // Display information Name string `json:"name" db:"name"` Description string `json:"description" db:"description"` // Tree organization Group int8 `json:"group" db:"group"` // AA tab (AA_CLASS, AA_SUBCLASS, etc.) Col int8 `json:"col" db:"col"` // Column position in tree Row int8 `json:"row" db:"row"` // Row position in tree // Visual representation Icon int16 `json:"icon" db:"icon"` // Primary icon ID Icon2 int16 `json:"icon2" db:"icon2"` // Secondary icon ID // Ranking system RankCost int8 `json:"rank_cost" db:"rank_cost"` // Cost per rank MaxRank int8 `json:"max_rank" db:"max_rank"` // Maximum achievable rank // Prerequisites MinLevel int8 `json:"min_level" db:"min_level"` // Minimum character level RankPrereqID int32 `json:"rank_prereq_id" db:"rank_prereq_id"` // Prerequisite AA node ID RankPrereq int8 `json:"rank_prereq" db:"rank_prereq"` // Required rank in prerequisite ClassReq int8 `json:"class_req" db:"class_req"` // Required class Tier int8 `json:"tier" db:"tier"` // AA tier ReqPoints int8 `json:"req_points" db:"req_points"` // Required points in classification ReqTreePoints int16 `json:"req_tree_points" db:"req_tree_points"` // Required points in entire tree // Display classification ClassName string `json:"class_name" db:"class_name"` // Class name for display SubclassName string `json:"subclass_name" db:"subclass_name"` // Subclass name for display LineTitle string `json:"line_title" db:"line_title"` // AA line title TitleLevel int8 `json:"title_level" db:"title_level"` // Title level requirement // Metadata CreatedAt time.Time `json:"created_at" db:"created_at"` UpdatedAt time.Time `json:"updated_at" db:"updated_at"` // Database connection db *database.Database isNew bool } // New creates a new alternate advancement with database connection func New(db *database.Database) *AltAdvancement { return &AltAdvancement{ CreatedAt: time.Now(), UpdatedAt: time.Now(), db: db, isNew: true, } } // Load loads an alternate advancement by node ID func Load(db *database.Database, nodeID int32) (*AltAdvancement, error) { aa := &AltAdvancement{ db: db, isNew: false, } 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 WHERE nodeid = ?` err := db.QueryRow(query, nodeID).Scan( &aa.NodeID, &aa.MinLevel, &aa.SpellCRC, &aa.Name, &aa.Description, &aa.Group, &aa.Icon, &aa.Icon2, &aa.Col, &aa.Row, &aa.RankCost, &aa.MaxRank, &aa.RankPrereqID, &aa.RankPrereq, &aa.ClassReq, &aa.Tier, &aa.ReqPoints, &aa.ReqTreePoints, &aa.LineTitle, &aa.TitleLevel, ) if err != nil { if err == sql.ErrNoRows { return nil, fmt.Errorf("alternate advancement not found: %d", nodeID) } return nil, fmt.Errorf("failed to load alternate advancement: %w", err) } // Set spell ID to node ID if not provided separately aa.SpellID = aa.NodeID return aa, nil } // LoadAll loads all alternate advancements from database func LoadAll(db *database.Database) ([]*AltAdvancement, 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.Query(query) if err != nil { return nil, fmt.Errorf("failed to query alternate advancements: %w", err) } defer rows.Close() var aas []*AltAdvancement for rows.Next() { aa := &AltAdvancement{ db: db, isNew: false, CreatedAt: time.Now(), UpdatedAt: time.Now(), } err := rows.Scan( &aa.NodeID, &aa.MinLevel, &aa.SpellCRC, &aa.Name, &aa.Description, &aa.Group, &aa.Icon, &aa.Icon2, &aa.Col, &aa.Row, &aa.RankCost, &aa.MaxRank, &aa.RankPrereqID, &aa.RankPrereq, &aa.ClassReq, &aa.Tier, &aa.ReqPoints, &aa.ReqTreePoints, &aa.LineTitle, &aa.TitleLevel, ) if err != nil { return nil, fmt.Errorf("failed to scan alternate advancement: %w", err) } // Set spell ID to node ID if not provided separately aa.SpellID = aa.NodeID aas = append(aas, aa) } return aas, rows.Err() } // Save saves the alternate advancement to the database (insert if new, update if existing) func (aa *AltAdvancement) Save() error { if aa.db == nil { return fmt.Errorf("no database connection") } tx, err := aa.db.Begin() if err != nil { return fmt.Errorf("failed to begin transaction: %w", err) } defer tx.Rollback() if aa.isNew { err = aa.insert(tx) } else { err = aa.update(tx) } if err != nil { return err } return tx.Commit() } // Delete removes the alternate advancement from the database func (aa *AltAdvancement) Delete() error { if aa.db == nil { return fmt.Errorf("no database connection") } if aa.isNew { return fmt.Errorf("cannot delete unsaved alternate advancement") } _, err := aa.db.Exec("DELETE FROM spell_aa_nodelist WHERE nodeid = ?", aa.NodeID) if err != nil { return fmt.Errorf("failed to delete alternate advancement: %w", err) } return nil } // Reload reloads the alternate advancement from the database func (aa *AltAdvancement) Reload() error { if aa.db == nil { return fmt.Errorf("no database connection") } if aa.isNew { return fmt.Errorf("cannot reload unsaved alternate advancement") } reloaded, err := Load(aa.db, aa.NodeID) if err != nil { return err } // Copy all fields from reloaded AA *aa = *reloaded return nil } // IsNew returns true if this is a new (unsaved) alternate advancement func (aa *AltAdvancement) IsNew() bool { return aa.isNew } // GetID returns the node ID (implements common.Identifiable interface) func (aa *AltAdvancement) GetID() int32 { return aa.NodeID } // IsValid validates the alternate advancement data func (aa *AltAdvancement) IsValid() bool { return aa.SpellID > 0 && aa.NodeID > 0 && len(aa.Name) > 0 && aa.MaxRank > 0 && aa.RankCost > 0 } // Clone creates a deep copy of the alternate advancement func (aa *AltAdvancement) Clone() *AltAdvancement { clone := &AltAdvancement{ SpellID: aa.SpellID, NodeID: aa.NodeID, SpellCRC: aa.SpellCRC, Name: aa.Name, Description: aa.Description, Group: aa.Group, Col: aa.Col, Row: aa.Row, Icon: aa.Icon, Icon2: aa.Icon2, RankCost: aa.RankCost, MaxRank: aa.MaxRank, MinLevel: aa.MinLevel, RankPrereqID: aa.RankPrereqID, RankPrereq: aa.RankPrereq, ClassReq: aa.ClassReq, Tier: aa.Tier, ReqPoints: aa.ReqPoints, ReqTreePoints: aa.ReqTreePoints, ClassName: aa.ClassName, SubclassName: aa.SubclassName, LineTitle: aa.LineTitle, TitleLevel: aa.TitleLevel, CreatedAt: aa.CreatedAt, UpdatedAt: aa.UpdatedAt, db: aa.db, isNew: false, } return clone } // Private helper methods func (aa *AltAdvancement) insert(tx *sql.Tx) error { query := `INSERT INTO spell_aa_nodelist (nodeid, minlevel, spellcrc, name, description, aa_list_fk, icon_id, icon_backdrop, xcoord, ycoord, pointspertier, maxtier, firstparentid, firstparentrequiredtier, displayedclassification, requiredclassification, classificationpointsrequired, pointsspentintreetounlock, title, titlelevel) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` _, err := tx.Exec(query, aa.NodeID, aa.MinLevel, aa.SpellCRC, aa.Name, aa.Description, aa.Group, aa.Icon, aa.Icon2, aa.Col, aa.Row, aa.RankCost, aa.MaxRank, aa.RankPrereqID, aa.RankPrereq, aa.ClassReq, aa.Tier, aa.ReqPoints, aa.ReqTreePoints, aa.LineTitle, aa.TitleLevel) if err != nil { return fmt.Errorf("failed to insert alternate advancement: %w", err) } aa.isNew = false return nil } func (aa *AltAdvancement) update(tx *sql.Tx) error { query := `UPDATE spell_aa_nodelist SET minlevel = ?, spellcrc = ?, name = ?, description = ?, aa_list_fk = ?, icon_id = ?, icon_backdrop = ?, xcoord = ?, ycoord = ?, pointspertier = ?, maxtier = ?, firstparentid = ?, firstparentrequiredtier = ?, displayedclassification = ?, requiredclassification = ?, classificationpointsrequired = ?, pointsspentintreetounlock = ?, title = ?, titlelevel = ? WHERE nodeid = ?` _, err := tx.Exec(query, aa.MinLevel, aa.SpellCRC, aa.Name, aa.Description, aa.Group, aa.Icon, aa.Icon2, aa.Col, aa.Row, aa.RankCost, aa.MaxRank, aa.RankPrereqID, aa.RankPrereq, aa.ClassReq, aa.Tier, aa.ReqPoints, aa.ReqTreePoints, aa.LineTitle, aa.TitleLevel, aa.NodeID) if err != nil { return fmt.Errorf("failed to update alternate advancement: %w", err) } return nil }