eq2go/internal/alt_advancement/alt_advancement.go
2025-08-07 12:47:52 -05:00

345 lines
9.4 KiB
Go

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
}